-
Notifications
You must be signed in to change notification settings - Fork 9
Add GitHub release asset upload action #743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
228263c
747d06f
291bee3
572ec20
d1d2011
448e959
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'fastlane/action' | ||
| require_relative '../../helper/github_helper' | ||
|
|
||
| module Fastlane | ||
| module Actions | ||
| class UploadGithubReleaseAssetsAction < Action | ||
| def self.run(params) | ||
| repository = params[:repository] | ||
| version = params[:version] | ||
| assets = params[:release_assets] | ||
| replace_existing = params[:replace_existing] | ||
|
|
||
| UI.message("Uploading #{assets.count} GitHub Release asset(s) to #{repository} #{version}.") | ||
|
|
||
| github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token]) | ||
| url = github_helper.upload_release_assets( | ||
| repository: repository, | ||
| version: version, | ||
| assets: assets, | ||
| replace_existing: replace_existing | ||
| ) | ||
|
|
||
| UI.success("Successfully uploaded GitHub Release assets. You can see the release at '#{url}'") | ||
| url | ||
| end | ||
|
|
||
| def self.description | ||
| 'Uploads assets to an existing GitHub Release' | ||
| end | ||
|
|
||
| def self.authors | ||
| ['Automattic'] | ||
| end | ||
|
|
||
| def self.return_value | ||
| 'The URL of the GitHub Release' | ||
| end | ||
|
|
||
| def self.details | ||
| 'Uploads assets to an existing GitHub Release. By default, existing release assets with matching filenames are replaced; when replace_existing is false, matching assets cause the action to fail.' | ||
| end | ||
|
|
||
| def self.available_options | ||
| [ | ||
| FastlaneCore::ConfigItem.new(key: :repository, | ||
| description: 'The slug (`<org>/<repo>`) of the GitHub repository containing the release', | ||
| optional: false, | ||
| type: String, | ||
| verify_block: proc do |value| | ||
| UI.user_error!('Repository cannot be empty') if value.to_s.empty? | ||
| end), | ||
| FastlaneCore::ConfigItem.new(key: :version, | ||
| description: 'The version of the release. Used as the git tag name', | ||
| optional: false, | ||
| type: String, | ||
| verify_block: proc do |value| | ||
| UI.user_error!('Version cannot be empty') if value.to_s.empty? | ||
| end), | ||
| FastlaneCore::ConfigItem.new(key: :release_assets, | ||
| description: 'Assets to upload', | ||
| type: Array, | ||
| optional: false, | ||
| verify_block: proc do |value| | ||
| UI.user_error!('You must provide at least one release asset') if value.nil? || value.empty? | ||
| value.each do |asset| | ||
| UI.user_error!('release_assets must contain file paths') unless asset.is_a?(String) && !asset.empty? | ||
| end | ||
| end), | ||
| FastlaneCore::ConfigItem.new(key: :replace_existing, | ||
| description: 'True to delete existing release assets with matching filenames before uploading. False to fail if a matching asset exists', | ||
| optional: true, | ||
| default_value: true, | ||
| type: Boolean), | ||
| Fastlane::Helper::GithubHelper.github_token_config_item, | ||
| ] | ||
| end | ||
|
|
||
| def self.is_supported?(platform) | ||
| true | ||
| end | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,57 @@ def create_release(repository:, version:, description:, assets:, prerelease:, is | |
| release[:html_url] | ||
| end | ||
|
|
||
| # Returns the GitHub release matching a given tag/version. | ||
| # | ||
| # @param [String] repository The repository to fetch the GitHub release from. Typically a repo slug (<org>/<repo>). | ||
| # @param [String] version The release version/tag to fetch. | ||
| # @return [Sawyer::Resource] The matching GitHub Release. | ||
| # @raise [Fastlane::UI::Error] UI.user_error! if the release does not exist. | ||
| # | ||
| def get_release(repository:, version:) | ||
| client.release_for_tag(repository, version) | ||
| rescue Octokit::NotFound | ||
| UI.user_error!("Could not find GitHub Release for tag #{version} in #{repository}") | ||
| end | ||
|
|
||
| # Uploads assets to an existing GitHub release, optionally replacing matching filenames. | ||
| # | ||
| # @param [String] repository The repository to upload the GitHub release assets to. Typically a repo slug (<org>/<repo>). | ||
| # @param [String] version The release version/tag to upload assets to. | ||
| # @param [Array<String>] assets List of local file paths to attach as release assets. | ||
| # @param [TrueClass|FalseClass] replace_existing Delete existing same-filename assets before uploading. When false, fail if a matching asset exists. | ||
| # @return [String] URL of the corresponding GitHub Release. | ||
| # @raise [Fastlane::UI::Error] UI.user_error! if the release or any local asset file does not exist. | ||
| # | ||
| 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 | ||
|
|
||
| # Use the GitHub API to generate release notes based on the list of PRs between current tag and previous tag. | ||
| # @note This API uses the `.github/release.yml` config file to classify the PRs by category in the generated list according to PR labels. | ||
| # | ||
|
|
@@ -368,6 +419,19 @@ def set_branch_protection(repository:, branch:, **options) | |
| client.protect_branch(repository, branch, options) | ||
| end | ||
|
|
||
| def validate_release_assets!(assets) | ||
| asset_paths = Array(assets) | ||
| UI.user_error!('You must provide at least one release asset') if asset_paths.empty? | ||
|
|
||
| asset_paths.each do |file_path| | ||
| UI.user_error!("Can't find file #{file_path}!") unless File.file?(file_path) | ||
| end | ||
|
Comment on lines
+424
to
+436
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copilot makes a good point. Added a failing test via #745 to track this refinement.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh it's a nice idea to add a failing test in a PR review like this 👍 thanks! I've merged it and CI is 🟢 |
||
|
|
||
| asset_paths | ||
| end | ||
|
|
||
| private :validate_release_assets! | ||
|
|
||
| # 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 | ||
| # @param [Sawyer::Resource] response The API response returned by `#get_branch_protection` or `#set_branch_protection` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'spec_helper' | ||
|
|
||
| describe Fastlane::Actions::UploadGithubReleaseAssetsAction do | ||
| let(:test_token) { 'ghp_fake_token' } | ||
| let(:test_repo) { 'repo-test/project-test' } | ||
| let(:test_version) { '1.0.0' } | ||
| let(:test_assets) { ['builds/test-app.zip'] } | ||
| let(:test_url) { 'https://github.com/repo-test/project-test/releases/tag/1.0.0' } | ||
| let(:github_helper) { instance_double(Fastlane::Helper::GithubHelper) } | ||
|
|
||
| before do | ||
| allow(Fastlane::Helper::GithubHelper).to receive(:new).with(github_token: test_token).and_return(github_helper) | ||
| end | ||
|
|
||
| it 'uploads release assets and returns the release URL' do | ||
| allow(github_helper).to receive(:upload_release_assets).and_return(test_url) | ||
| expect(github_helper).to receive(:upload_release_assets).with( | ||
| repository: test_repo, | ||
| version: test_version, | ||
| assets: test_assets, | ||
| replace_existing: true | ||
| ) | ||
|
|
||
| result = run_described_fastlane_action( | ||
| github_token: test_token, | ||
| repository: test_repo, | ||
| version: test_version, | ||
| release_assets: test_assets | ||
| ) | ||
|
|
||
| expect(result).to eq(test_url) | ||
| end | ||
|
|
||
| it 'forwards replace_existing when provided' do | ||
| allow(github_helper).to receive(:upload_release_assets).and_return(test_url) | ||
| expect(github_helper).to receive(:upload_release_assets).with( | ||
| repository: test_repo, | ||
| version: test_version, | ||
| assets: test_assets, | ||
| replace_existing: false | ||
| ) | ||
|
|
||
| result = run_described_fastlane_action( | ||
| github_token: test_token, | ||
| repository: test_repo, | ||
| version: test_version, | ||
| release_assets: test_assets, | ||
| replace_existing: false | ||
| ) | ||
|
|
||
| expect(result).to eq(test_url) | ||
| end | ||
|
|
||
| it 'fails when release_assets is empty' do | ||
| expect do | ||
| run_described_fastlane_action( | ||
| github_token: test_token, | ||
| repository: test_repo, | ||
| version: test_version, | ||
| release_assets: [] | ||
| ) | ||
| end.to raise_error(FastlaneCore::Interface::FastlaneError, 'You must provide at least one release asset') | ||
| end | ||
|
|
||
| it 'fails when release_assets contains a non-path value' do | ||
| expect do | ||
| run_described_fastlane_action( | ||
| github_token: test_token, | ||
| repository: test_repo, | ||
| version: test_version, | ||
| release_assets: [123] | ||
| ) | ||
| end.to raise_error(FastlaneCore::Interface::FastlaneError, 'release_assets must contain file paths') | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This risks overriding files with the same name but from different folders (
ios/app.ipaandtvos/app.ipa).Is this a legitimate concern? If so, what's a good way to address this? Compare using the full path stripped of the machine-specific path to the project folder maybe?
I added a test for this in #745