fastlane-plugin-buildstash 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +165 -0
- data/lib/fastlane/plugin/buildstash/actions/buildstash_upload_action.rb +465 -0
- data/lib/fastlane/plugin/buildstash/helper/buildstash_helper.rb +109 -0
- data/lib/fastlane/plugin/buildstash/version.rb +5 -0
- data/lib/fastlane/plugin/buildstash.rb +16 -0
- metadata +185 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 62cba9cd5be4ef6fb4a9b5dac48500b080449a60a8cf0791c17e3bdf2444721c
|
|
4
|
+
data.tar.gz: 749f043fdb116a03536e9702ee1cd902681c7f47699ce1207eb93e6f13824cfa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2291b1f0b91c5aed6563d98875b2419c0ffe97c832c5444a6b614b11580cc7ae0c6262a355acbbfc6ccbffc156185488241a0056a6d5f0acab7ad64ec38a925f
|
|
7
|
+
data.tar.gz: 592499bfd9c1a466dd43680dddf0ee1e71d3432c9b3da17f44201c923051ec9f6ab82da786d4e38092e5a706cccca21f111a4dfb8c83e9f8522aa71cc6b8fbd2
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Buildstash, and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Fastlane Buildstash Plugin
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The `fastlane-plugin-buildstash` plugin allows you to upload build artifacts to Buildstash seamlessly as part of your Fastlane workflow. This plugin ensures that your builds are efficiently stored and accessible for further processing.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
You can install this plugin to your project running:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
fastlane add_plugin buildstash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or, to use a copy of this plugin locally, add it to your `Pluginfile`:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'fastlane-plugin-buildstash', path: '/path/to/fastlane-plugin-buildstash'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then run:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
To upload an artifact to Buildstash, use the `buildstash_upload` action in your `Fastfile`.
|
|
27
|
+
|
|
28
|
+
With base required parameters:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
buildstash_upload(
|
|
32
|
+
api_key: 'BUILDSTASH_APP_API_KEY',
|
|
33
|
+
primary_file_path: './path/to/file.apk',
|
|
34
|
+
platform: 'android',
|
|
35
|
+
stream: 'default',
|
|
36
|
+
version_component_1_major: 0,
|
|
37
|
+
version_component_2_minor: 0,
|
|
38
|
+
version_component_3_patch: 1
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or with all input parameters:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
lane :run_buildstash_upload do |options|
|
|
46
|
+
buildstash_upload(
|
|
47
|
+
api_key: options[:api_key],
|
|
48
|
+
structure: 'file',
|
|
49
|
+
primary_file_path: './path/to/file.apk',
|
|
50
|
+
platform: 'android',
|
|
51
|
+
stream: 'default',
|
|
52
|
+
version_component_1_major: 0,
|
|
53
|
+
version_component_2_minor: 0,
|
|
54
|
+
version_component_3_patch: 1,
|
|
55
|
+
version_component_extra: 'rc',
|
|
56
|
+
version_component_meta: '2024.12.01',
|
|
57
|
+
custom_build_number: '12345',
|
|
58
|
+
notes: '<AppChangelog>',
|
|
59
|
+
source: 'ghactions',
|
|
60
|
+
ci_pipeline: options[:ci_pipeline],
|
|
61
|
+
ci_run_id: options[:ci_run_id],
|
|
62
|
+
ci_run_url: options[:ci_run_url],
|
|
63
|
+
vc_host_type: 'git',
|
|
64
|
+
vc_host: 'github',
|
|
65
|
+
vc_repo_name: options[:vc_repo_name],
|
|
66
|
+
vc_repo_url: options[:vc_repo_url],
|
|
67
|
+
vc_branch: options[:vc_branch],
|
|
68
|
+
vc_commit_sha: options[:vc_commit_sha],
|
|
69
|
+
vc_commit_url: options[:vc_commit_url]
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Parameters
|
|
75
|
+
| Parameter | Description | Required |
|
|
76
|
+
|--------------|--------------------------------------------------------------------------------------------------------------|----------|
|
|
77
|
+
| `api_key` | The API key for authentication | ✅ |
|
|
78
|
+
| `structure` | 'file' for single file, 'file+expansion' to include Android expansion file. will default to 'file' | ✖ |
|
|
79
|
+
| `primary_file_path` | './path/to/file.apk' | ✅ |
|
|
80
|
+
| `platform` | 'android' or 'ios' (see [Buildstash docs for full list](https://docs.buildstash.com/integrations/platforms)) | ✅ |
|
|
81
|
+
| `stream` | Exact name of a build stream in your app | ✅ |
|
|
82
|
+
| `version_component_1_major` | Semantic version (major component) | ✅ |
|
|
83
|
+
| `version_component_2_minor` | Semantic version (minor component) | ✅ |
|
|
84
|
+
| `version_component_3_patch` | Semantic version (patch component) | ✅ |
|
|
85
|
+
| `version_component_extra` | Optional pre-release label (beta, rc1, etc) | ✖ |
|
|
86
|
+
| `version_component_meta` | Optional release metadata | ✖ |
|
|
87
|
+
| `custom_build_number` | Optional custom build number in any format | ✖ |
|
|
88
|
+
| `notes` | Changelog or additional notes | ✖️ |
|
|
89
|
+
| `source` | Where build was produced (`ghactions`, `jenkins`, etc) defaults to cli-upload | ✖️ |
|
|
90
|
+
| `ci_pipeline` | CI pipeline name | ✖️ |
|
|
91
|
+
| `ci_run_id` | CI run ID | ✖️ |
|
|
92
|
+
| `ci_run_url` | CI run URL | ✖️ |
|
|
93
|
+
| `vc_host_type` | Version control host type (git, svn, hg, perforce, etc) | ✖️ |
|
|
94
|
+
| `vc_host` | Version control host (github, gitlab, etc) | ✖️ |
|
|
95
|
+
| `vc_repo_name` | Repository name | ✖️ |
|
|
96
|
+
| `vc_repo_url` | Repository URL | ✖️ |
|
|
97
|
+
| `vc_branch` | Branch name (if applicable) | ✖️ |
|
|
98
|
+
| `vc_commit_sha` | Commit SHA (if applicable) | ✖️ |
|
|
99
|
+
| `vc_commit_url` | Commit URL | ✖️ |
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Example Output
|
|
103
|
+
When the upload is successful, you will see:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
[✔] Upload to Buildstash successful!
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If the file does not exist, an error will be raised:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
[✗] File not found at path: non_existent_file.apk
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## Outputs
|
|
117
|
+
The buildstash_upload action stores the following outputs in lane_context for in subsequent actions:
|
|
118
|
+
|
|
119
|
+
| Key | Description |
|
|
120
|
+
|-----|-------------|
|
|
121
|
+
| `BUILDSTASH_BUILD_ID` | The build ID in Buildstash for the uploaded build |
|
|
122
|
+
| `BUILDSTASH_INFO_URL` | Link to view uploaded build within Buildstash workspace |
|
|
123
|
+
| `BUILDSTASH_DOWNLOAD_URL` | Link to download the build uploaded to Buildstash (requires login) |
|
|
124
|
+
|
|
125
|
+
For example, to see these values output:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
lane :test_plugin do |options|
|
|
129
|
+
buildstash_upload(
|
|
130
|
+
api_key: options[:api_key],
|
|
131
|
+
primary_file_path: "ponderpad.ipa",
|
|
132
|
+
platform: "ios",
|
|
133
|
+
stream: "default",
|
|
134
|
+
version_component_1_major: 1,
|
|
135
|
+
version_component_2_minor: 0,
|
|
136
|
+
version_component_3_patch: 1
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Output to terminal
|
|
140
|
+
UI.message("🔧 Buildstash Build ID: #{lane_context[:BUILDSTASH_BUILD_ID]}")
|
|
141
|
+
UI.message("🔗 Build Info URL: #{lane_context[:BUILDSTASH_INFO_URL]}")
|
|
142
|
+
UI.message("📦 Download URL: #{lane_context[:BUILDSTASH_DOWNLOAD_URL]}")
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Testing
|
|
147
|
+
To run tests:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
bundle exec rspec
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
1. Fork the repository.
|
|
155
|
+
2. Create a new branch (`feature/my-feature`).
|
|
156
|
+
3. Commit your changes.
|
|
157
|
+
4. Push to the branch and create a Merge Request.
|
|
158
|
+
|
|
159
|
+
Contributions are welcome.
|
|
160
|
+
|
|
161
|
+
## Support
|
|
162
|
+
For issues and feature requests, please contact the internal development team or submit an issue on GitLab.
|
|
163
|
+
|
|
164
|
+
## Thanks
|
|
165
|
+
Credit to [Yann Miecielica](https://github.com/yMiecie) and the team at [Gimbal Cube](https://us.gimbalcube.com/) for contributions to this plugin.
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/buildstash_helper'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Fastlane
|
|
7
|
+
module Actions
|
|
8
|
+
class BuildstashUploadAction < Action
|
|
9
|
+
def self.run(params)
|
|
10
|
+
api_key = params[:api_key]
|
|
11
|
+
structure = params[:structure]
|
|
12
|
+
primary_file_path = params[:primary_file_path]
|
|
13
|
+
version_component_1_major = params[:version_component_1_major]
|
|
14
|
+
version_component_2_minor = params[:version_component_2_minor]
|
|
15
|
+
version_component_3_patch = params[:version_component_3_patch]
|
|
16
|
+
version_component_extra = params[:version_component_extra]
|
|
17
|
+
version_component_meta = params[:version_component_meta]
|
|
18
|
+
custom_build_number = params[:custom_build_number]
|
|
19
|
+
platform = params[:platform]
|
|
20
|
+
stream = params[:stream]
|
|
21
|
+
notes = params[:notes]
|
|
22
|
+
|
|
23
|
+
source = params[:source]
|
|
24
|
+
|
|
25
|
+
ci_pipeline = params[:ci_pipeline]
|
|
26
|
+
ci_run_id = params[:ci_run_id]
|
|
27
|
+
ci_run_url = params[:ci_run_url]
|
|
28
|
+
|
|
29
|
+
vc_host_type = params[:vc_host_type]
|
|
30
|
+
vc_host = params[:vc_host]
|
|
31
|
+
vc_repo_name = params[:vc_repo_name]
|
|
32
|
+
vc_repo_url = params[:vc_repo_url]
|
|
33
|
+
vc_branch = params[:vc_branch]
|
|
34
|
+
vc_commit_sha = params[:vc_commit_sha]
|
|
35
|
+
vc_commit_url = params[:vc_commit_url]
|
|
36
|
+
|
|
37
|
+
if !structure
|
|
38
|
+
structure = "file"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if !source
|
|
42
|
+
source = "cli-upload"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
UI.user_error!("File path must be provided.") if primary_file_path.to_s.strip.empty?
|
|
46
|
+
UI.user_error!("File not found at path: #{primary_file_path}") unless File.exist?(primary_file_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
UI.message("Send upload request...")
|
|
50
|
+
|
|
51
|
+
file_size = File.size(primary_file_path)
|
|
52
|
+
file_name = File.basename(primary_file_path)
|
|
53
|
+
|
|
54
|
+
request_body = {
|
|
55
|
+
structure: structure,
|
|
56
|
+
primary_file: {
|
|
57
|
+
filename: file_name,
|
|
58
|
+
size_bytes: file_size
|
|
59
|
+
},
|
|
60
|
+
version_component_1_major: version_component_1_major,
|
|
61
|
+
version_component_2_minor: version_component_2_minor,
|
|
62
|
+
version_component_3_patch: version_component_3_patch,
|
|
63
|
+
version_component_extra: version_component_extra,
|
|
64
|
+
version_component_meta: version_component_meta,
|
|
65
|
+
custom_build_number: custom_build_number,
|
|
66
|
+
platform: platform,
|
|
67
|
+
stream: stream,
|
|
68
|
+
notes: notes,
|
|
69
|
+
source: source,
|
|
70
|
+
ci_pipeline: ci_pipeline,
|
|
71
|
+
ci_run_id: ci_run_id,
|
|
72
|
+
ci_run_url: ci_run_url,
|
|
73
|
+
vc_host_type: vc_host_type,
|
|
74
|
+
vc_host: vc_host,
|
|
75
|
+
vc_repo_name: vc_repo_name,
|
|
76
|
+
vc_repo_url: vc_repo_url,
|
|
77
|
+
vc_branch: vc_branch,
|
|
78
|
+
vc_commit_sha: vc_commit_sha,
|
|
79
|
+
vc_commit_url: vc_commit_url
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expansion_file_path = params[:expansion_file_path]
|
|
83
|
+
# Add expansion file info if structure is file+expansion and expansion file path provided
|
|
84
|
+
if structure == 'file+expansion' && expansion_file_path
|
|
85
|
+
|
|
86
|
+
# Verify expansion file exists
|
|
87
|
+
unless File.exist?(expansion_file_path)
|
|
88
|
+
UI.user_error!("Expansion file not found at path: #{expansion_file_path}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get expansion file stats
|
|
92
|
+
expansion_filename = File.basename(expansion_file_path)
|
|
93
|
+
expansion_file_size = File.size(expansion_file_path)
|
|
94
|
+
|
|
95
|
+
request_body[:expansion_files] = [{
|
|
96
|
+
filename: expansion_filename,
|
|
97
|
+
size_bytes: expansion_file_size
|
|
98
|
+
}]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
response = Helper::BuildstashHelper.post_json(
|
|
102
|
+
url: "https://app.buildstash.com/api/v1/upload/request",
|
|
103
|
+
body: request_body,
|
|
104
|
+
headers: {
|
|
105
|
+
"Authorization" => "Bearer #{api_key}",
|
|
106
|
+
"Content-Type" => "application/json",
|
|
107
|
+
"Accept" => "application/json"
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
112
|
+
UI.error("Buildstash API returned #{response.code}: #{response.body}")
|
|
113
|
+
UI.user_error!("Buildstash API request failed")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if response.content_type && response.content_type != "application/json"
|
|
117
|
+
UI.user_error!("Upload request failed due to unexpected response type: #{response.content_type} - Response: #{response.code}: #{response.body}")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
response_data = JSON.parse(response.body)
|
|
121
|
+
|
|
122
|
+
UI.verbose("Response data: #{response_data.inspect}")
|
|
123
|
+
|
|
124
|
+
# Verify if the response contains an error
|
|
125
|
+
if response_data["errors"]
|
|
126
|
+
UI.user_error!("Buildstash API Error: #{response_data["message"]} - Response: #{response.code}: #{response.body}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
pending_upload_id = response_data["pending_upload_id"]
|
|
130
|
+
primary_file = response_data["primary_file"]
|
|
131
|
+
expansion_files = response_data["expansion_files"]
|
|
132
|
+
|
|
133
|
+
# Handle primary file upload
|
|
134
|
+
if primary_file["chunked_upload"]
|
|
135
|
+
UI.message('Uploading primary file using chunked upload...');
|
|
136
|
+
primary_file_parts = Helper::BuildstashHelper.upload_chunked_file(
|
|
137
|
+
file_path: primary_file_path,
|
|
138
|
+
filesize: file_size,
|
|
139
|
+
pending_upload_id: pending_upload_id,
|
|
140
|
+
chunk_count: primary_file["chunked_number_parts"],
|
|
141
|
+
chunk_size_mb: primary_file["chunked_part_size_mb"],
|
|
142
|
+
api_key: api_key,
|
|
143
|
+
is_expansion: false
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
UI.verbose("primary_file_parts=#{primary_file_parts}");
|
|
147
|
+
else
|
|
148
|
+
UI.message("Uploading primary file using direct upload...")
|
|
149
|
+
|
|
150
|
+
response = Helper::BuildstashHelper.upload_file(
|
|
151
|
+
url: primary_file["presigned_data"]["url"],
|
|
152
|
+
file_path: primary_file_path,
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Disposition" => primary_file["presigned_data"]["headers"]["Content-Disposition"],
|
|
155
|
+
"x-amz-acl": "private",
|
|
156
|
+
"Content-Type" => primary_file["presigned_data"]["headers"]["Content-Type"],
|
|
157
|
+
"Content-Length" => file_size.to_s
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
162
|
+
UI.user_error!("Upload failed #{response.code}: #{response.body}")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
UI.message("Upload done! Response code: #{response.code}")
|
|
166
|
+
UI.message("Response body: #{response.body}")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if pending_upload_id.nil? || pending_upload_id.empty?
|
|
170
|
+
UI.user_error!("Invalid pending_upload_id received from Buildstash.")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if expansion_file_path && response_data["expansion_files"] && response_data["expansion_files"][0]
|
|
174
|
+
expansion_info = response_data["expansion_files"][0]
|
|
175
|
+
expansion_file_size = File.size(expansion_file_path)
|
|
176
|
+
|
|
177
|
+
if expansion_info["chunked_upload"]
|
|
178
|
+
UI.message("Uploading expansion file using chunked upload...")
|
|
179
|
+
|
|
180
|
+
expansion_parts = Helper::BuildstashHelper.upload_chunked_file(
|
|
181
|
+
file_path: expansion_file_path,
|
|
182
|
+
filesize: expansion_file_size,
|
|
183
|
+
pending_upload_id: response_data["pending_upload_id"],
|
|
184
|
+
chunk_count: expansion_info["chunked_number_parts"],
|
|
185
|
+
chunk_size_mb: expansion_info["chunked_part_size_mb"],
|
|
186
|
+
api_key: params[:api_key],
|
|
187
|
+
is_expansion: true
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Store this info for later if needed
|
|
191
|
+
# e.g. for upload/complete or logs
|
|
192
|
+
UI.success("Expansion file uploaded in #{expansion_parts.size} parts.")
|
|
193
|
+
UI.message("Expansion parts: #{expansion_parts.map { |p| p[:PartNumber] }.join(', ')}")
|
|
194
|
+
else
|
|
195
|
+
UI.message("Uploading expansion file using direct upload...")
|
|
196
|
+
|
|
197
|
+
response = Helper::BuildstashHelper.upload_file(
|
|
198
|
+
url: expansion_info["presigned_data"]["url"],
|
|
199
|
+
file_path: expansion_file_path,
|
|
200
|
+
headers: {
|
|
201
|
+
"Content-Type" => expansion_info["presigned_data"]["headers"]["Content-Type"],
|
|
202
|
+
"Content-Length" => expansion_info["presigned_data"]["headers"]["Content-Length"].to_s,
|
|
203
|
+
"Content-Disposition" => expansion_info["presigned_data"]["headers"]["Content-Disposition"],
|
|
204
|
+
"x-amz-acl" => "private"
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
209
|
+
UI.user_error!("Expansion file upload failed: #{response.code} #{response.body}")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
UI.success("Expansion file uploaded successfully.")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
UI.message("Verifying upload...")
|
|
217
|
+
|
|
218
|
+
verify_body = {
|
|
219
|
+
pending_upload_id: pending_upload_id
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if defined?(primary_file_parts) && primary_file_parts && !primary_file_parts.empty?
|
|
223
|
+
verify_body[:multipart_chunks] = primary_file_parts
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# add expansion parts to the verify payload if they exist
|
|
227
|
+
if defined?(expansion_parts) && expansion_parts && !expansion_parts.empty?
|
|
228
|
+
verify_body[:multipart_chunks] ||= []
|
|
229
|
+
verify_body[:multipart_chunks].concat(expansion_parts)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
response = Helper::BuildstashHelper.post_json(
|
|
233
|
+
url: "https://app.buildstash.com/api/v1/upload/verify",
|
|
234
|
+
body: verify_body,
|
|
235
|
+
headers: {
|
|
236
|
+
"Authorization" => "Bearer #{api_key}",
|
|
237
|
+
"Content-Type" => "application/json",
|
|
238
|
+
"Accept" => "application/json"
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
242
|
+
UI.error("Buildstash API returned #{response.code}: #{response.body}")
|
|
243
|
+
UI.user_error!("Buildstash API request failed")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if response.content_type && response.content_type != "application/json"
|
|
247
|
+
UI.user_error!("Verification failed due to unexpected response type: #{response.content_type} - Response: #{response.code}: #{response.body}")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
response_data = JSON.parse(response.body)
|
|
251
|
+
|
|
252
|
+
# Set outputs
|
|
253
|
+
Actions.lane_context[:BUILDSTASH_BUILD_ID] = response_data["build_id"]
|
|
254
|
+
Actions.lane_context[:BUILDSTASH_INFO_URL] = response_data["build_info_url"]
|
|
255
|
+
Actions.lane_context[:BUILDSTASH_DOWNLOAD_URL] = response_data["download_url"]
|
|
256
|
+
|
|
257
|
+
if response_data["build_info_url"] && response_data&.dig("pending_processing") == true
|
|
258
|
+
UI.success("✅ Upload complete! Build now being processed. Once ready, view it at: #{response_data["build_info_url"]}")
|
|
259
|
+
elsif response_data["build_info_url"]
|
|
260
|
+
UI.success("✅ Upload complete! View it at: #{response_data["build_info_url"]}")
|
|
261
|
+
else
|
|
262
|
+
UI.success("✅ Upload to Buildstash successful!")
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def self.description
|
|
267
|
+
"Upload build artifacts to Buildstash"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def self.available_options
|
|
271
|
+
[
|
|
272
|
+
FastlaneCore::ConfigItem.new(
|
|
273
|
+
key: :api_key,
|
|
274
|
+
description: "Buildstash API key",
|
|
275
|
+
optional: false,
|
|
276
|
+
type: String
|
|
277
|
+
),
|
|
278
|
+
|
|
279
|
+
FastlaneCore::ConfigItem.new(
|
|
280
|
+
key: :structure,
|
|
281
|
+
description: "Upload structure: 'file' or 'file+expansion'",
|
|
282
|
+
optional: true,
|
|
283
|
+
type: String,
|
|
284
|
+
default_value: "file"
|
|
285
|
+
),
|
|
286
|
+
|
|
287
|
+
FastlaneCore::ConfigItem.new(
|
|
288
|
+
key: :primary_file_path,
|
|
289
|
+
description: "Path to the primary file to upload",
|
|
290
|
+
optional: false,
|
|
291
|
+
type: String,
|
|
292
|
+
verify_block: proc do |value|
|
|
293
|
+
UI.user_error!("File not found: #{value}") unless File.exist?(value)
|
|
294
|
+
end
|
|
295
|
+
),
|
|
296
|
+
|
|
297
|
+
FastlaneCore::ConfigItem.new(
|
|
298
|
+
key: :platform,
|
|
299
|
+
description: "Platform of the build",
|
|
300
|
+
optional: false,
|
|
301
|
+
type: String
|
|
302
|
+
),
|
|
303
|
+
|
|
304
|
+
FastlaneCore::ConfigItem.new(
|
|
305
|
+
key: :stream,
|
|
306
|
+
description: "Buildstash stream",
|
|
307
|
+
optional: false,
|
|
308
|
+
type: String
|
|
309
|
+
),
|
|
310
|
+
|
|
311
|
+
FastlaneCore::ConfigItem.new(
|
|
312
|
+
key: :version_component_1_major,
|
|
313
|
+
description: "Semantic version (major component)",
|
|
314
|
+
optional: false,
|
|
315
|
+
type: Integer
|
|
316
|
+
),
|
|
317
|
+
|
|
318
|
+
FastlaneCore::ConfigItem.new(
|
|
319
|
+
key: :version_component_2_minor,
|
|
320
|
+
description: "Semantic version (minor component)",
|
|
321
|
+
optional: false,
|
|
322
|
+
type: Integer
|
|
323
|
+
),
|
|
324
|
+
|
|
325
|
+
FastlaneCore::ConfigItem.new(
|
|
326
|
+
key: :version_component_3_patch,
|
|
327
|
+
description: "Semantic version (patch component)",
|
|
328
|
+
optional: false,
|
|
329
|
+
type: Integer
|
|
330
|
+
),
|
|
331
|
+
|
|
332
|
+
FastlaneCore::ConfigItem.new(
|
|
333
|
+
key: :version_component_extra,
|
|
334
|
+
description: "Additional version identifier (e.g., `rc`)",
|
|
335
|
+
optional: true,
|
|
336
|
+
type: String
|
|
337
|
+
),
|
|
338
|
+
|
|
339
|
+
FastlaneCore::ConfigItem.new(
|
|
340
|
+
key: :version_component_meta,
|
|
341
|
+
description: "Metadata related to the version",
|
|
342
|
+
optional: true,
|
|
343
|
+
type: String
|
|
344
|
+
),
|
|
345
|
+
|
|
346
|
+
FastlaneCore::ConfigItem.new(
|
|
347
|
+
key: :custom_build_number,
|
|
348
|
+
description: "Custom build number",
|
|
349
|
+
optional: true,
|
|
350
|
+
type: String
|
|
351
|
+
),
|
|
352
|
+
|
|
353
|
+
FastlaneCore::ConfigItem.new(
|
|
354
|
+
key: :notes,
|
|
355
|
+
description: "Changelog or additional notes",
|
|
356
|
+
optional: true,
|
|
357
|
+
type: String
|
|
358
|
+
),
|
|
359
|
+
|
|
360
|
+
FastlaneCore::ConfigItem.new(
|
|
361
|
+
key: :expansion_file_path,
|
|
362
|
+
description: "Path to the expansion file (if there is one)",
|
|
363
|
+
optional: true,
|
|
364
|
+
type: String
|
|
365
|
+
),
|
|
366
|
+
|
|
367
|
+
FastlaneCore::ConfigItem.new(
|
|
368
|
+
key: :source,
|
|
369
|
+
description: "Where build was produced (e.g., `ghactions`, `jenkins`, etc.)",
|
|
370
|
+
optional: true,
|
|
371
|
+
type: String,
|
|
372
|
+
),
|
|
373
|
+
|
|
374
|
+
FastlaneCore::ConfigItem.new(
|
|
375
|
+
key: :ci_pipeline,
|
|
376
|
+
description: "CI pipeline name",
|
|
377
|
+
optional: true,
|
|
378
|
+
type: String,
|
|
379
|
+
),
|
|
380
|
+
|
|
381
|
+
FastlaneCore::ConfigItem.new(
|
|
382
|
+
key: :ci_run_id,
|
|
383
|
+
description: "CI run ID",
|
|
384
|
+
optional: true,
|
|
385
|
+
type: String,
|
|
386
|
+
),
|
|
387
|
+
|
|
388
|
+
FastlaneCore::ConfigItem.new(
|
|
389
|
+
key: :ci_run_url,
|
|
390
|
+
description: "CI run URL",
|
|
391
|
+
optional: true,
|
|
392
|
+
type: String,
|
|
393
|
+
),
|
|
394
|
+
|
|
395
|
+
FastlaneCore::ConfigItem.new(
|
|
396
|
+
key: :vc_host_type,
|
|
397
|
+
description: "Version control host type (git, svn, hg, perforce, etc)",
|
|
398
|
+
optional: true,
|
|
399
|
+
type: String,
|
|
400
|
+
),
|
|
401
|
+
|
|
402
|
+
FastlaneCore::ConfigItem.new(
|
|
403
|
+
key: :vc_host,
|
|
404
|
+
description: "Version control host (github, gitlab, etc)",
|
|
405
|
+
optional: true,
|
|
406
|
+
type: String,
|
|
407
|
+
),
|
|
408
|
+
|
|
409
|
+
FastlaneCore::ConfigItem.new(
|
|
410
|
+
key: :vc_repo_name,
|
|
411
|
+
description: "Repository name",
|
|
412
|
+
optional: true,
|
|
413
|
+
type: String,
|
|
414
|
+
),
|
|
415
|
+
|
|
416
|
+
FastlaneCore::ConfigItem.new(
|
|
417
|
+
key: :vc_repo_url,
|
|
418
|
+
description: "Repository URL",
|
|
419
|
+
optional: true,
|
|
420
|
+
type: String,
|
|
421
|
+
),
|
|
422
|
+
|
|
423
|
+
FastlaneCore::ConfigItem.new(
|
|
424
|
+
key: :vc_branch,
|
|
425
|
+
description: "Branch name (if applicable)",
|
|
426
|
+
optional: true,
|
|
427
|
+
type: String,
|
|
428
|
+
),
|
|
429
|
+
|
|
430
|
+
FastlaneCore::ConfigItem.new(
|
|
431
|
+
key: :vc_commit_sha,
|
|
432
|
+
description: "Commit SHA (if applicable)",
|
|
433
|
+
optional: true,
|
|
434
|
+
type: String,
|
|
435
|
+
),
|
|
436
|
+
|
|
437
|
+
FastlaneCore::ConfigItem.new(
|
|
438
|
+
key: :vc_commit_url,
|
|
439
|
+
description: "Commit URL",
|
|
440
|
+
optional: true,
|
|
441
|
+
type: String,
|
|
442
|
+
),
|
|
443
|
+
|
|
444
|
+
]
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def self.output
|
|
448
|
+
[
|
|
449
|
+
['BUILDSTASH_BUILD_ID', 'The build ID in Buildstash for the uploaded build'],
|
|
450
|
+
['BUILDSTASH_INFO_URL', 'Link to view uploaded build within Buildstash workspace'],
|
|
451
|
+
['BUILDSTASH_DOWNLOAD_URL', 'Link to download the build uploaded to Buildstash (requires login)']
|
|
452
|
+
]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def self.author
|
|
456
|
+
'Buildstash'
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def self.is_supported?(platform)
|
|
460
|
+
true
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
|
|
5
|
+
|
|
6
|
+
module Helper
|
|
7
|
+
class BuildstashHelper
|
|
8
|
+
# class methods that you define here become available in your action
|
|
9
|
+
# as `Helper::BuildstashHelper.your_method`
|
|
10
|
+
#
|
|
11
|
+
def self.show_message
|
|
12
|
+
UI.message("Hello from the buildstash plugin helper!")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.post_json(url:, body:, headers:)
|
|
16
|
+
uri = URI(url)
|
|
17
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
18
|
+
http.use_ssl = (uri.scheme == "https")
|
|
19
|
+
request = Net::HTTP::Post.new(uri.path, headers)
|
|
20
|
+
request.body = body.to_json
|
|
21
|
+
http.request(request)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.upload_file(url:, file_path:, headers:)
|
|
25
|
+
uri = URI(url)
|
|
26
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
27
|
+
http.use_ssl = (uri.scheme == "https")
|
|
28
|
+
request = Net::HTTP::Put.new(uri.request_uri, headers)
|
|
29
|
+
request.body = File.binread(file_path)
|
|
30
|
+
http.request(request)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.upload_chunked_file(file_path:, filesize:, pending_upload_id:, chunk_count:, chunk_size_mb:, api_key:, is_expansion: false)
|
|
34
|
+
parts = []
|
|
35
|
+
chunk_size = chunk_size_mb * 1024 * 1024
|
|
36
|
+
endpoint = is_expansion ?
|
|
37
|
+
"https://app.buildstash.com/api/v1/upload/request/multipart/expansion" :
|
|
38
|
+
"https://app.buildstash.com/api/v1/upload/request/multipart"
|
|
39
|
+
|
|
40
|
+
File.open(file_path, 'rb') do |file|
|
|
41
|
+
chunk_count.times do |i|
|
|
42
|
+
chunk_start = i * chunk_size
|
|
43
|
+
chunk_end = [((i + 1) * chunk_size) - 1, filesize - 1].min
|
|
44
|
+
content_length = chunk_end - chunk_start + 1
|
|
45
|
+
part_number = i + 1
|
|
46
|
+
|
|
47
|
+
puts "Uploading chunked upload, part: #{part_number} of #{chunk_count}"
|
|
48
|
+
|
|
49
|
+
# Request for signed URL for the part
|
|
50
|
+
uri = URI(endpoint)
|
|
51
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
52
|
+
http.use_ssl = true
|
|
53
|
+
req = Net::HTTP::Post.new(uri.path, {
|
|
54
|
+
"Authorization" => "Bearer #{api_key}",
|
|
55
|
+
"Content-Type" => "application/json",
|
|
56
|
+
"Accept" => "application/json"
|
|
57
|
+
})
|
|
58
|
+
req.body = {
|
|
59
|
+
pending_upload_id: pending_upload_id,
|
|
60
|
+
part_number: part_number,
|
|
61
|
+
content_length: content_length
|
|
62
|
+
}.to_json
|
|
63
|
+
|
|
64
|
+
response = http.request(req)
|
|
65
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
66
|
+
raise "Failed to get presigned URL for part #{part_number}: #{response.code} #{response.body}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
data = JSON.parse(response.body)
|
|
70
|
+
presigned_url = data["part_presigned_url"]
|
|
71
|
+
|
|
72
|
+
# Read the chunk data from the file
|
|
73
|
+
file.seek(chunk_start)
|
|
74
|
+
chunk_data = file.read(content_length)
|
|
75
|
+
|
|
76
|
+
# Upload the chunk to the presigned URL
|
|
77
|
+
uri = URI(presigned_url)
|
|
78
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
79
|
+
http.use_ssl = true
|
|
80
|
+
|
|
81
|
+
upload = Net::HTTP::Put.new(uri.request_uri, {
|
|
82
|
+
"Content-Type" => "application/octet-stream",
|
|
83
|
+
"Content-Length" => content_length.to_s
|
|
84
|
+
})
|
|
85
|
+
upload.body = chunk_data
|
|
86
|
+
|
|
87
|
+
upload_response = http.request(upload)
|
|
88
|
+
|
|
89
|
+
unless upload_response.is_a?(Net::HTTPSuccess)
|
|
90
|
+
raise "Upload failed for part #{part_number}: #{upload_response.code} #{upload_response.body}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
etag = upload_response["ETag"]
|
|
94
|
+
if etag.nil?
|
|
95
|
+
puts "⚠️ No ETag returned for part #{part_number}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
parts << {
|
|
99
|
+
PartNumber: part_number,
|
|
100
|
+
ETag: etag&.gsub('"', '')
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
parts
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'fastlane/plugin/buildstash/version'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module Buildstash
|
|
5
|
+
# Return all .rb files inside the "actions" and "helper" directory
|
|
6
|
+
def self.all_classes
|
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# By default we want to import all available actions and helpers
|
|
13
|
+
# A plugin can contain any number of actions and plugins
|
|
14
|
+
Fastlane::Buildstash.all_classes.each do |current|
|
|
15
|
+
require current
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-buildstash
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Buildstash
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fastlane
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 2.205.2
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.205.2
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pry
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rspec_junit_formatter
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - '='
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.12.1
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - '='
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 1.12.1
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubocop-performance
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rubocop-require_tools
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: simplecov
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
152
|
+
email: hello@buildstash.com
|
|
153
|
+
executables: []
|
|
154
|
+
extensions: []
|
|
155
|
+
extra_rdoc_files: []
|
|
156
|
+
files:
|
|
157
|
+
- LICENSE
|
|
158
|
+
- README.md
|
|
159
|
+
- lib/fastlane/plugin/buildstash.rb
|
|
160
|
+
- lib/fastlane/plugin/buildstash/actions/buildstash_upload_action.rb
|
|
161
|
+
- lib/fastlane/plugin/buildstash/helper/buildstash_helper.rb
|
|
162
|
+
- lib/fastlane/plugin/buildstash/version.rb
|
|
163
|
+
homepage: https://github.com/buildstash/fastlane-plugin-buildstash
|
|
164
|
+
licenses:
|
|
165
|
+
- MIT
|
|
166
|
+
metadata: {}
|
|
167
|
+
rdoc_options: []
|
|
168
|
+
require_paths:
|
|
169
|
+
- lib
|
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
171
|
+
requirements:
|
|
172
|
+
- - ">="
|
|
173
|
+
- !ruby/object:Gem::Version
|
|
174
|
+
version: '2.5'
|
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
180
|
+
requirements: []
|
|
181
|
+
rubygems_version: 3.6.7
|
|
182
|
+
specification_version: 4
|
|
183
|
+
summary: Fastlane plugin to upload built apps to Buildstash - store, organize, and
|
|
184
|
+
distribute your builds.
|
|
185
|
+
test_files: []
|