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



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



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

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:



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 451

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



483
484
485
486
487
488
489
490
491
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 483

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



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 338

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

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



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

  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, name: 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 (/).

  • version (String)

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

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

    The display name (title) of the GitHub release. Defaults to the version if not provided.

  • 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



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

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



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 317

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.

Parameters:

  • repository (String)

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

  • head (String)

    The head branch to look for. May be given as branch or as the fully-qualified owner:branch; when unqualified, it is automatically prefixed with the repository's owner.

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

    The base branch the PR should target. If nil, PRs targeting any base are considered.

  • state (String) (defaults to: 'open')

    The PR state to match (open, closed, or all). Defaults to open.

Returns:

  • (Sawyer::Resource, nil)

    The first matching Pull Request, or nil if none matches.



368
369
370
371
372
373
374
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 368

def find_pull_request(repository:, head:, base: nil, state: 'open')
  qualified_head = head.include?(':') ? head : "#{repository.split('/').first}:#{head}"
  options = { state: state, head: qualified_head }
  options[:base] = base unless base.nil?

  client.pull_requests(repository, options).first
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 (/).

  • 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



259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 259

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:



408
409
410
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 408

def get_branch_protection(repository:, branch:, **options)
  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)
  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) ⇒ Sawyer::Resource

Note:

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.

Parameters:

  • repository (String)

    A GitHub repository slug

  • release (String)

    The release version to find the milestone for.

Returns:

  • (Sawyer::Resource)

    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

Parameters:

  • repository (String)

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

  • milestone (Sawyer::Resource, String)

    The milestone object, or title of the milestone, we want to fetch the list of PRs for (e.g.: 16.9)

  • include_closed (Boolean) (defaults to: false)

    If set to true, will include both opened and closed PRs. Otherwise, will only include opened PRs.

Returns:

  • (Array<Sawyer::Resource>)

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



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.

Parameters:

  • repository (String)

    The repository to fetch the GitHub release from. Typically a repo slug (/).

  • version (String)

    The release version/tag to fetch.

Returns:

  • (Sawyer::Resource)

    The matching GitHub Release.

Raises:

  • (Fastlane::UI::Error)

    UI.user_error! if the release does not exist.



202
203
204
205
206
207
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 202

def get_release(repository:, version:)
  release = client.releases(repository).find { |candidate| candidate.tag_name == 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

Parameters:

  • repository (String)

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

  • 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.



278
279
280
281
282
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 278

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

Publishes an existing GitHub Release still in draft mode.

Parameters:

  • repository (String)

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

  • name (String)

    The name of the release to publish.

  • prerelease (Boolean) (defaults to: nil)

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

Returns:

  • (String)

    URL of the corresponding GitHub Release



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 292

def publish_release(repository:, name:, prerelease: nil)
  releases = client.releases(repository)
  release = releases.find { |r| r.name == name }

  UI.user_error!("No release found with name #{name}") unless release

  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

Parameters:

  • repository (String)

    The repository name (including the organization)

  • branch (String)

    The branch name

See Also:



398
399
400
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 398

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

Parameters:

  • repository (String)

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

  • milestone (Sawyer::Resource, String)

    The milestone object, or title of the milestone

  • type (Symbol)

    The type of items to search for (:issue or :pr/:pull_request)

  • include_closed (Boolean) (defaults to: false)

    If set to true, will include both opened and closed items. Otherwise, will only include opened items.

Returns:

  • (Array<Sawyer::Resource>)

    A list of issues or PRs for the 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

Parameters:

  • repository (String)

    The repository name (including the organization)

  • branch (String)

    The branch name

  • options (Hash)

    A customizable set of options.

See Also:



419
420
421
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 419

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

#set_milestone(repository:, number:, milestone:) ⇒ Object

Note:

Use get_milestone to get a milestone object from a version number

Set/Update the milestone assigned to a given PR or issue

Parameters:

  • repository (String)

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

  • number (Integer)

    The PR (or issue) number to update the milestone of

  • milestone (Sawyer::Resource?, Integer?)

    The milestone object or number to set on this PR, or nil to unset the milestone

Raises:

  • (Fastlane::UI::Error)

    UI.user_error! if PR does not exist or milestone provided is invalid



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

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:



388
389
390
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 388

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

#upload_release_assets(repository:, version:, assets:, replace_existing: true) ⇒ String

Uploads assets to an existing GitHub release, optionally replacing matching filenames.

Parameters:

  • repository (String)

    The repository to upload the GitHub release assets to. Typically a repo slug (/).

  • version (String)

    The release version/tag to upload assets to.

  • assets (Array<String>)

    List of local file paths to attach as release assets.

  • replace_existing (TrueClass|FalseClass) (defaults to: true)

    Delete existing same-filename assets before uploading. When false, fail if a matching asset exists.

Returns:

  • (String)

    URL of the corresponding GitHub Release.

Raises:

  • (Fastlane::UI::Error)

    UI.user_error! if the release or any local asset file does not exist.



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.message("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.message("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