-
Notifications
You must be signed in to change notification settings - Fork 9
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
Add Upload to S3 Action #339
Conversation
9252e41
to
3d112b2
Compare
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.
Why not use https://github.com/fastlane-community/fastlane-plugin-s3 instead of reimplementing our own?!
module Fastlane | ||
module Actions | ||
module SharedValues | ||
UPLOADED_FILE_PATH = :UPLOADED_FILE_PATH |
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.
Given SharedValues
is a wide and quite global namespace I'd suggest use a more specific constant name here.
UPLOADED_FILE_PATH = :UPLOADED_FILE_PATH | |
S3_UPLOADED_FILE_PATH = :S3_UPLOADED_FILE_PATH |
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.
Addressed in 1dcc36b to also update its usage.
Good question – I'd be fine to use it if we could (though I'm curious whether it'd make more sense to have this plugin depend on that one, or just use both in the top-level app – I don't feel strongly either way). I came across this issue which I believe is a blocker, but I'll try it out in a branch and see if it's actually been resolved already (as the thread indicates it might have been). |
@AliSoftware – I took another look at using the built-in S3 plugin, and there are a few other issues (from what I can see):
|
Co-authored-by: Olivier Halligon <[email protected]>
1dcc36b
to
98b47d1
Compare
Rebased to fix merge conflicts. |
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.
Sad that there's no tests for this action, but there's not much to do when the bulk of the logic is around a third party dependency.
Well, actually, it looks like the AWS SDK has stubbing methods for testing. It might be worth checking them out, but definitely not worth blocking this PR for me, also given this is required the Woo Android installable builds.
I implicitly verified this code's happy path in woocommerce/woocommerce-android#5862. Approving.
Aws::S3::Client.new().put_object({ | ||
body: file, | ||
bucket: bucket, | ||
key: key | ||
}) |
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.
Indentation nitpick.
Aws::S3::Client.new().put_object({ | |
body: file, | |
bucket: bucket, | |
key: key | |
}) | |
Aws::S3::Client.new().put_object( | |
{ | |
body: file, | |
bucket: bucket, | |
key: key | |
} | |
) |
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.
Or, more compact:
Aws::S3::Client.new().put_object({ | |
body: file, | |
bucket: bucket, | |
key: key | |
}) | |
Aws::S3::Client.new().put_object({ body: file, bucket: bucket, key: key }) |
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.
I even think, given how Ruby works and interprets named parameters at call site vs Hashes at def site, that you should be able to do:
Aws::S3::Client.new().put_object({ | |
body: file, | |
bucket: bucket, | |
key: key | |
}) | |
Aws::S3::Client.new().put_object(body: file, bucket: bucket, key: key) |
At least that's how most of those typical Ruby client implementations (e.g. Octokit for GitHub API client) expect you to call those methods anyway 🙂
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.
Yep – it's funny, I reviewed another PR and suggested dropping the hash syntax, but somehow missed it here. I've simplified it to just the regular named parameter syntax (though I retained the multi-line call in case of future changes).
bucket: bucket, | ||
key: key | ||
) | ||
return (response[:content_length]).positive? |
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.
Is the wrapping in ( )
necessary?
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.
☝️
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.
Probably not! 😅
Removed in ab376e7
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
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.
Left a couple of style nitpicks, otherwise seems to be working.
I still think unit tests would be quite valuable to that action, despite it interacting with a 3rd party service.
We can stub_requests
or make use of double
/ instance_double
and similar to easily stub the S3 side of things, and focus the tests on the behavior at parsing and interpreting the input ConfigItems
params and checking that it calls put_object
with the expected parameters as a result.
For example, writing a test that calls run_described_fastlane_action
with a nil
key, and checks that it would either auto-generate a key for us, or crash with a user_error!
if that is not permitted (I actually have no idea which of the 2 behaviors would be the correct one for that case, hence using that as a good example for a spec 😛 )
That being said, if that is a blocker, we could also decide to merge this PR without the tests in order to unblock the migration of remaining app projects to Buildkite, and then implement the tests in a subsequent PR, but I still think we should implement those tests at some point.
Aws::S3::Client.new().put_object({ | ||
body: file, | ||
bucket: bucket, | ||
key: key | ||
}) |
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.
I even think, given how Ruby works and interprets named parameters at call site vs Hashes at def site, that you should be able to do:
Aws::S3::Client.new().put_object({ | |
body: file, | |
bucket: bucket, | |
key: key | |
}) | |
Aws::S3::Client.new().put_object(body: file, bucket: bucket, key: key) |
At least that's how most of those typical Ruby client implementations (e.g. Octokit for GitHub API client) expect you to call those methods anyway 🙂
bucket: bucket, | ||
key: key | ||
) | ||
return (response[:content_length]).positive? |
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.
☝️
FastlaneCore::ConfigItem.new( | ||
key: :key, | ||
description: 'The path to the file within the bucket', | ||
optional: true, |
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.
Optional? 🤔 What happens if we don't provide a key parameter to that one?
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.
Good q – addressed in the testing PR, but now it'll emit an error.
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
def self.run(params) | ||
file_path = params[:file] | ||
file_name = File.basename(file_path) | ||
UI.user_error!("Unable to read file at #{file_path}") unless File.file?(file_path) |
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.
You can move this to the verify_block
of the ConfigItem
(see this suggestion) to let Fastlane report the error earlier in the process while parsing args (without having to create a Runner for nothing)
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.
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.
Actually found a bug while giving a try at implementing Unit Tests in #340 😉
key = params[:key] | ||
|
||
if params[:auto_prefix] == true | ||
file_name_hash = Digest::SHA1.base64digest(file_name) |
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.
Note: the Base64 alphabet happens to contain the /
character. Will that risk causing an issue or result in misleading or strange cases?
For example if the input file is named input_file_1
then the base64 digest of its SHA1 is k5w5OY2yQF55HiBXeP9w+F3/Yg4=
with a /
in the middle. Worse, I imagine there could be input file names that lead to a base64 SHA1 to end with a /
, which would lead to a key containing two consecutive /
once we assemble that prefix with the filename, and I can imagine a key with foo//bar
to lead to errors with S3…
Discovered that use case by implementing Unit Tests for this in #340 btw… so they are really useful (in case that was still needed to be proven) 😛
I suggest we should use hexdigest
instead of base64digest
to avoid such situations:
file_name_hash = Digest::SHA1.base64digest(file_name) | |
file_name_hash = Digest::SHA1.hexdigest(file_name) |
Introduce specs for upload_to_s3 action
Co-authored-by: Gio Lodi <[email protected]> Co-authored-by: Olivier Halligon <[email protected]>
If that happens, it'll just be nested in "directories" within S3. Doesn't hurt anything, but there's a possibility of a crash if the string ends with |
@AliSoftware – I'll wait to ensure your concerns are addressed before merging if that's ok? |
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.
Overall LGTM by skimming the new code 👍 but left a couple of suggestions to improve the wording, error messages, and code readability.
I'll let you address those nits during your day, and review the PR again first thing on my tomorrow — at which point I expect we should finally be all good to go 🤞
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
Outdated
Show resolved
Hide resolved
spec/spec_helper.rb
Outdated
# Executes the given block with a temporary file | ||
def with_tmp_file_path | ||
in_tmp_dir do |tmp_dir| | ||
file_name = ('a'..'z').to_a.sample(8).join # 8-character random file name | ||
file_path = File.join(tmp_dir, file_name) | ||
|
||
File.write(file_path, '') | ||
yield file_path | ||
File.delete(file_path) | ||
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.
I think it would be both simpler (and DRY) and more readable to merge those two helpers in a single one, with a file name generated using sample(8)
like you did if the file provided as parameter is nil
(which could be the default). That would look like this:
# Executes the given block with a temporary file with the given `file_name`
def with_tmp_file(named: nil, content: '')
in_tmp_dir do |tmp_dir|
file_name = named || ('a'..'z').to_a.sample(8).join # 8-character random file name if nil
file_path = File.join(tmp_dir, file_name)
File.write(file_path, content)
yield file_path
ensure
File.delete(file_path)
end
end
And then you'd simply replace all your calls to with_tmp_file_path
with this with_tmp_file
(without parameters), and all your calls to with_tmp_file_path_for_file_named(x)
with with_tmp_file(named: x)
.
spec/spec_helper.rb
Outdated
in_tmp_dir do |tmp_dir| | ||
file_path = File.join(tmp_dir, file_name) | ||
|
||
File.write(file_path, '') |
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.
I'd suggest to provide an optional content parameter to the method to allow custom content to be written as well instead of an empty file.
i.e. making the signature of the method look more like: def with_tmp_file(basename:, content: '')
and then use the content
parameter on this line.
spec/upload_to_s3_spec.rb
Outdated
stub_s3_head_request(key, 0) | ||
end | ||
|
||
describe 'uploading a file' do |
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 ends up being the only describe
at that level, which makes it kinda pointless, and also containing quite a lot of tests in a single group.
I'd suggest you to split it instead, for example:
describe 'use the proper key'
(L28-L117)describe 'errors on invalid input'
(the rest)
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.
Fixed in 281c181 – I tried to keep to the "reads as a sentence" format
Co-authored-by: Olivier Halligon <[email protected]>
), | ||
FastlaneCore::ConfigItem.new( | ||
key: :key, | ||
description: 'The path to the file within the bucket. If `nil`, will default to the `file`'s basename', |
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.
🚫 | unexpected token tIDENTIFIER (Using Ruby 2.6 parser; configure using TargetRubyVersion parameter, under AllCops ) |
Adds an "upload to S3" action inside the release toolkit that's useful for binary storage (particularly for installable builds).
To Test:
Note that woocommerce/woocommerce-android#5862 passes and properly displays a comment with a download link – the upload functionality for this is powered by this new action.