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 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,5 @@
1
+ module Fastlane
2
+ module Buildstash
3
+ VERSION = "1.0.0"
4
+ end
5
+ 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: []