shopify-cli 2.29.0 → 2.30.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/project_types/theme/commands/package.rb +20 -5
  5. data/lib/project_types/theme/messages/messages.rb +4 -2
  6. data/lib/shopify_cli/packager.rb +5 -14
  7. data/lib/shopify_cli/theme/backoff_helper.rb +47 -0
  8. data/lib/shopify_cli/theme/ignore_helper.rb +7 -1
  9. data/lib/shopify_cli/theme/syncer/downloader.rb +63 -0
  10. data/lib/shopify_cli/theme/syncer/uploader/bulk.rb +133 -0
  11. data/lib/shopify_cli/theme/syncer/uploader/bulk_item.rb +64 -0
  12. data/lib/shopify_cli/theme/syncer/uploader/bulk_job.rb +139 -0
  13. data/lib/shopify_cli/theme/syncer/uploader/bulk_request.rb +30 -0
  14. data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all.rb +41 -0
  15. data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all_form.rb +37 -0
  16. data/lib/shopify_cli/theme/syncer/uploader/forms/base_strategy_form.rb +64 -0
  17. data/lib/shopify_cli/theme/syncer/uploader/forms/select_delete_strategy.rb +29 -0
  18. data/lib/shopify_cli/theme/syncer/uploader/forms/select_update_strategy.rb +30 -0
  19. data/lib/shopify_cli/theme/syncer/uploader/json_delete_handler.rb +49 -0
  20. data/lib/shopify_cli/theme/syncer/uploader/json_update_handler.rb +71 -0
  21. data/lib/shopify_cli/theme/syncer/uploader.rb +227 -0
  22. data/lib/shopify_cli/theme/syncer.rb +91 -144
  23. data/lib/shopify_cli/version.rb +1 -1
  24. metadata +16 -16
  25. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +0 -39
  26. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +0 -35
  27. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +0 -62
  28. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +0 -27
  29. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +0 -28
  30. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +0 -51
  31. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +0 -96
  32. data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk.rb +0 -102
  33. data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk_job.rb +0 -75
  34. data/lib/shopify_cli/theme/theme_admin_api_throttler/errors.rb +0 -7
  35. data/lib/shopify_cli/theme/theme_admin_api_throttler/put_request.rb +0 -52
  36. data/lib/shopify_cli/theme/theme_admin_api_throttler/request_parser.rb +0 -39
  37. data/lib/shopify_cli/theme/theme_admin_api_throttler/response_parser.rb +0 -21
  38. data/lib/shopify_cli/theme/theme_admin_api_throttler.rb +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f07c45a6fc9d6b93d9f2826a7ad9eb139842d8b7f35717ca5fb17291285c330
4
- data.tar.gz: 774e222e76f9a8d30b68e8b0756ed4cecbc8281d302d94c131d072c123b19f8c
3
+ metadata.gz: 3050c5be253ff04fbd3fa04ddd60389c0e6c0c2c0a3ae3fdcc6b059dc428c193
4
+ data.tar.gz: 2d373f27b01e11cd4df467114ba00849c978ae7cca1d8529749be5ba5d5d663d
5
5
  SHA512:
6
- metadata.gz: eab29689be4de3a23b0b9dd14401a130d6f37cc1064c02f16bbce0656d378d487666bd964dfa673b8fcc48344b01acf8d10899e4fb3755c7f5f46c5dd8c46385
7
- data.tar.gz: 01d9830606768a70de1de312527875d997dce26f1913c44fd6ffd82407afc7962931fe38dd7568e49c899438e33bf3f1b465b6b5d73bb0585d25643c0d26d5a2
6
+ metadata.gz: 8427e448b97accd935d7b6b16452070139a70139d53cfd3e4d7f4efff495873f4d1372e9e08bf4b75fae7150c95ddb431363b7d970dbcfffc1fbeb1b22f7c0af
7
+ data.tar.gz: be22490e0dea78184b7bcd58c4bca6a1a52c2963c45063e9a7ad6703ecd295c6e31d37a02716f741a3a7682d0fc12f86a167b0d78d1ef9bd973894bc5b7b4ba1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@ From version 2.6.0, the sections in this file adhere to the [keep a changelog](h
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## Version 2.30.0 - 2022-11-01
6
+
7
+ ### Fixed
8
+ * [#2668](https://github.com/Shopify/shopify-cli/pull/2668): Introduce `--only/--ignore` in the `shopify theme serve` help message
9
+ * [#2667](https://github.com/Shopify/shopify-cli/pull/2667): Fix for "X zip is required for packaging a theme" on Windows
10
+
5
11
  ## Version 2.29.0 - 2022-10-19
6
12
 
7
13
  ### Added
@@ -11,6 +17,7 @@ From version 2.6.0, the sections in this file adhere to the [keep a changelog](h
11
17
 
12
18
  ### Fixed
13
19
  * [#2646](https://github.com/Shopify/shopify-cli/pull/2646): Demo themes shouldn't appear in the `shopify theme pull/push/list/open` commands
20
+ * [#2650](https://github.com/Shopify/shopify-cli/pull/2650): The `shopify theme push`/`shopify theme serve` commands no longer freeze in some scenarios
14
21
 
15
22
  ### Changed
16
23
  * [#2648](https://github.com/Shopify/shopify-cli/pull/2648): Do not warn users when the CLI 2.x is running as a subprocess
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shopify-cli (2.29.0)
4
+ shopify-cli (2.30.0)
5
5
  bugsnag (~> 6.22)
6
6
  listen (~> 3.7.0)
7
7
  theme-check (~> 1.11.0)
@@ -18,11 +18,15 @@ module Theme
18
18
  release-notes.md
19
19
  ]
20
20
 
21
+ ZIP = "zip"
22
+ SEVEN_ZIP = "7z"
23
+
21
24
  def call(args, _name)
22
25
  path = args.first || "."
23
26
 
24
- check_prereq_command("zip")
27
+ check_prereq_command
25
28
  zip_name = theme_name(path) + ".zip"
29
+
26
30
  zip(zip_name, path, THEME_DIRECTORIES)
27
31
  @ctx.done(@ctx.message("theme.package.done", zip_name))
28
32
  end
@@ -33,13 +37,12 @@ module Theme
33
37
 
34
38
  private
35
39
 
36
- def check_prereq_command(command)
37
- cmd_path = @ctx.which(command)
38
- @ctx.abort(@ctx.message("theme.package.error.prereq_command_required", command)) if cmd_path.nil?
40
+ def check_prereq_command
41
+ @ctx.abort(@ctx.message("theme.package.error.prereq_command_required")) if command.nil?
39
42
  end
40
43
 
41
44
  def zip(zip_name, path, files)
42
- @ctx.system("zip", "-r", zip_name, *files, chdir: path)
45
+ @ctx.system(command, command_flags, zip_name, *files, chdir: path)
43
46
  end
44
47
 
45
48
  def theme_name(path)
@@ -53,6 +56,18 @@ module Theme
53
56
 
54
57
  [theme_name, theme_info["theme_version"]].compact.join("-")
55
58
  end
59
+
60
+ def command
61
+ @command ||= if @ctx.which(ZIP)
62
+ ZIP
63
+ elsif @ctx.which(SEVEN_ZIP)
64
+ SEVEN_ZIP
65
+ end
66
+ end
67
+
68
+ def command_flags
69
+ @command_flags ||= command == ZIP ? "-r" : "a"
70
+ end
56
71
  end
57
72
  end
58
73
  end
@@ -124,6 +124,8 @@ module Theme
124
124
 
125
125
  Options:
126
126
  {{command:-t, --theme=NAME_OR_ID}} Theme ID or name of the remote theme.
127
+ {{command:-o, --only}} Hot reload only files that match the specified pattern.
128
+ {{command:-x, --ignore}} Skip hot reloading any files that match the specified pattern.
127
129
  {{command:--port=PORT}} Local port to serve theme preview from.
128
130
  {{command:--poll}} Force polling to detect file changes.
129
131
  {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
@@ -300,8 +302,8 @@ module Theme
300
302
  Usage: {{command:%s theme package [ ROOT ]}}
301
303
  HELP
302
304
  error: {
303
- prereq_command_required: "%1$s is required for packaging a theme. Please install %1$s "\
304
- "using the appropriate package manager for your system.",
305
+ prereq_command_required: "zip or 7zip is required for packaging a theme. Please install "\
306
+ "zip or 7zip using the appropriate package manager for your system.",
305
307
  missing_config: "Provide a config/settings_schema.json to package your theme",
306
308
  missing_theme_name: "Provide a theme_info.theme_name configuration in config/settings_schema.json",
307
309
  },
@@ -69,14 +69,13 @@ module ShopifyCLI
69
69
  def build_homebrew
70
70
  root_dir = File.join(PACKAGING_DIR, "homebrew")
71
71
 
72
- build_path = File.join(BUILDS_DIR, "shopify-cli.rb")
73
- build_path_2 = File.join(BUILDS_DIR, "shopify-cli@2.rb")
74
- puts "\nBuilding Homebrew packages"
72
+ build_path = File.join(BUILDS_DIR, "shopify-cli@2.rb")
73
+ puts "\nBuilding Homebrew package"
75
74
 
76
- puts "Generating formulae…"
75
+ puts "Generating formula…"
77
76
  File.delete(build_path) if File.exist?(build_path)
78
77
 
79
- spec_contents = File.read(File.join(root_dir, "shopify-cli.base.rb"))
78
+ spec_contents = File.read(File.join(root_dir, "shopify-cli@2.base.rb"))
80
79
  spec_contents = spec_contents.gsub("SHOPIFY_CLI_VERSION", ShopifyCLI::VERSION)
81
80
 
82
81
  puts "Grabbing sha256 checksum from Rubygems.org"
@@ -90,15 +89,7 @@ module ShopifyCLI
90
89
  spec_contents = spec_contents.gsub("SHOPIFY_CLI_GEM_CHECKSUM", gem_checksum)
91
90
 
92
91
  puts "Writing generated formula\n To: #{build_path}\n\n"
93
- File.write(build_path, spec_contents.gsub("SHOPIFY_CLI_BINSTUB_SUFFIX", ""))
94
-
95
- puts "Writing generated formula\n To: #{build_path_2}\n\n"
96
- File.write(
97
- build_path_2,
98
- spec_contents
99
- .sub("class ShopifyCli < Formula", "class ShopifyCliAT2 < Formula")
100
- .sub("SHOPIFY_CLI_BINSTUB_SUFFIX", "2")
101
- )
92
+ File.write(build_path, spec_contents)
102
93
  end
103
94
 
104
95
  private
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ module BackoffHelper
6
+ def initialize_backoff_helper!(margin: 2, backoff_delay: 2)
7
+ @margin = margin
8
+ @backoff_delay = backoff_delay
9
+ @backoff_mutex = Mutex.new
10
+ end
11
+
12
+ def backoff_if_near_limit!(response)
13
+ # Check if the API told us we're near the rate limit
14
+ return if backingoff? || !response
15
+
16
+ limit = response.fetch("x-shopify-shop-api-call-limit", "0/999")
17
+ used, total = limit.split("/").map(&:to_i)
18
+
19
+ backoff! if used > total - @margin
20
+ end
21
+
22
+ def wait_for_backoff!
23
+ # Sleeping in the mutex in another thread. Wait for unlock
24
+ backoff_mutex.synchronize {} if backingoff?
25
+ end
26
+
27
+ def backoff!
28
+ ctx.debug("Near API call limit, waiting #{@backoff_delay} seconds")
29
+ backoff_mutex.synchronize { wait(@backoff_delay) }
30
+ end
31
+
32
+ def backingoff?
33
+ backoff_mutex.locked?
34
+ end
35
+
36
+ def backoff_mutex
37
+ @backoff_mutex || raise("Backoff helper must be initialized")
38
+ end
39
+
40
+ private
41
+
42
+ def wait(seconds)
43
+ sleep(seconds)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -14,7 +14,13 @@ module ShopifyCLI
14
14
  end
15
15
 
16
16
  def ignore_path?(path)
17
- ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
17
+ is_ignored = ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
18
+
19
+ if is_ignored && @ctx
20
+ @ctx.debug("ignore #{path}")
21
+ end
22
+
23
+ is_ignored
18
24
  end
19
25
 
20
26
  private
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Downloader
9
+ extend Forwardable
10
+
11
+ attr_reader :syncer, :ctx
12
+
13
+ def_delegators :syncer,
14
+ :ctx,
15
+ :checksums,
16
+ :enqueue_get,
17
+ :ignore_file?,
18
+ :fetch_checksums!,
19
+ :wait!
20
+
21
+ def initialize(syncer, delete, &update_progress_bar_block)
22
+ @syncer = syncer
23
+ @delete = delete
24
+ @update_progress_bar_block = update_progress_bar_block
25
+ end
26
+
27
+ def download!
28
+ fetch_checksums!
29
+
30
+ if delete_local_files?
31
+ to_be_deleted.each { |file| delete(file) }
32
+ end
33
+
34
+ enqueue_get(checksums.keys)
35
+ wait!(&@update_progress_bar_block)
36
+ end
37
+
38
+ private
39
+
40
+ def delete(file)
41
+ ctx.debug("[#{self.class}] rm #{file.relative_path}")
42
+ file.delete
43
+ end
44
+
45
+ def delete_local_files?
46
+ @delete
47
+ end
48
+
49
+ def to_be_deleted
50
+ @to_be_deleted ||= syncer
51
+ .theme
52
+ .theme_files
53
+ .reject { |file| present_remotely?(file) }.uniq
54
+ .reject { |file| ignore_file?(file) }
55
+ end
56
+
57
+ def present_remotely?(file)
58
+ checksums.has?(file)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool"
4
+ require "shopify_cli/theme/backoff_helper"
5
+
6
+ require_relative "bulk_job"
7
+
8
+ module ShopifyCLI
9
+ module Theme
10
+ class Syncer
11
+ class Uploader
12
+ class Bulk
13
+ include ShopifyCLI::Theme::BackoffHelper
14
+
15
+ MAX_BULK_BYTESIZE = 102_400 # 100KB
16
+ MAX_BULK_FILES = 10 # 10 files
17
+ SHUTDOWN_TIMEOUT = 20 # 20 seconds
18
+ SHUTDOWN_RETRY_INTERVAL = 0.2 # 200 milliseconds
19
+
20
+ attr_reader :ctx, :theme, :admin_api
21
+
22
+ def initialize(ctx, theme, admin_api, pool_size: 15)
23
+ @ctx = ctx
24
+ @theme = theme
25
+ @admin_api = admin_api
26
+ @pending_items = []
27
+ @in_progress_items = []
28
+
29
+ # Mutex used to coordinate changes in the `@pending_items` shared
30
+ # accross the `@thread_pool` threads
31
+ @pending_items_mutex = Mutex.new
32
+
33
+ # Mutex used to coordinate changes in the `@in_progress_items`
34
+ # shared accross the `@thread_pool` threads
35
+ @in_progress_items_mutex = Mutex.new
36
+
37
+ # Initialize thread pool and the jobs
38
+ @thread_pool = initialize_thread_pool!(pool_size)
39
+
40
+ # Initialize backoff helper on main thread to pause all jobs when
41
+ # requests are reaching API rate limits
42
+ initialize_backoff_helper!(margin: pool_size, backoff_delay: 5)
43
+ end
44
+
45
+ def enqueue(bulk_item)
46
+ @pending_items_mutex.synchronize { @pending_items << bulk_item }
47
+ end
48
+
49
+ def shutdown
50
+ wait_bulk_items
51
+ @thread_pool.shutdown
52
+ end
53
+
54
+ def consume_bulk_items
55
+ items = []
56
+ items_bytesize = 0
57
+
58
+ @pending_items_mutex.synchronize do
59
+ has_enough_items = false
60
+ has_enough_bytesize = false
61
+
62
+ until has_enough_items || has_enough_bytesize || @pending_items.empty?
63
+ bulk_item = @pending_items.first
64
+
65
+ has_enough_items = items.size + 1 > MAX_BULK_FILES
66
+ has_enough_bytesize = items_bytesize + bulk_item.size > MAX_BULK_BYTESIZE
67
+
68
+ break if items.any? && (has_enough_items || has_enough_bytesize)
69
+
70
+ items << bulk_item
71
+ items_bytesize += bulk_item.size
72
+
73
+ @pending_items.shift
74
+ end
75
+ end
76
+
77
+ @in_progress_items_mutex.synchronize do
78
+ @in_progress_items += items
79
+ end
80
+
81
+ [items, items_bytesize]
82
+ end
83
+
84
+ def clean_in_progress_items(items)
85
+ @in_progress_items_mutex.synchronize do
86
+ @in_progress_items -= items
87
+ end
88
+ end
89
+
90
+ def remaining_items
91
+ @pending_items + @in_progress_items
92
+ end
93
+
94
+ private
95
+
96
+ def wait_bulk_items
97
+ start_time = Time.now
98
+
99
+ wait(SHUTDOWN_RETRY_INTERVAL) while remaining_items? && start_time - Time.now < SHUTDOWN_TIMEOUT
100
+
101
+ files = remaining_items.map { |item| "- #{item.key}" }.join("\n")
102
+
103
+ log("shutdown, remaining_items=#{remaining_items.size}\n#{files}")
104
+ end
105
+
106
+ def remaining_items?
107
+ !@pending_items.empty? || !@in_progress_items.empty?
108
+ end
109
+
110
+ def initialize_thread_pool!(pool_size)
111
+ ShopifyCLI::ThreadPool
112
+ .new(pool_size: pool_size)
113
+ .tap do |thread_pool|
114
+ pool_size.times { thread_pool.schedule(spawn_job) }
115
+ end
116
+ end
117
+
118
+ def spawn_job
119
+ BulkJob.new(ctx, self)
120
+ end
121
+
122
+ def log(message)
123
+ ctx.debug("[Bulk] #{message}")
124
+ end
125
+
126
+ def wait(seconds)
127
+ sleep(seconds)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool/job"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Uploader
9
+ class BulkItem
10
+ attr_reader :file, :block
11
+ attr_accessor :retries
12
+
13
+ def initialize(file, &block)
14
+ @file = file
15
+ @block = block
16
+ @retries = 0
17
+ end
18
+
19
+ def to_h
20
+ { body: body }
21
+ end
22
+
23
+ def to_s
24
+ "#<#{self.class.name} key=#{key}, retries=#{retries}>"
25
+ end
26
+
27
+ def liquid?
28
+ file.liquid?
29
+ end
30
+
31
+ def key
32
+ file.relative_path
33
+ end
34
+
35
+ def size
36
+ @size ||= body.bytesize
37
+ end
38
+
39
+ def body
40
+ @body ||= JSON.generate(asset: asset)
41
+ end
42
+
43
+ def asset
44
+ @asset ||= asset_hash
45
+ end
46
+
47
+ private
48
+
49
+ def asset_hash
50
+ asset = { key: file.relative_path }
51
+
52
+ if file.text?
53
+ asset[:value] = file.read
54
+ else
55
+ asset[:attachment] = Base64.encode64(file.read)
56
+ end
57
+
58
+ asset
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool/job"
4
+
5
+ require_relative "bulk_request"
6
+
7
+ module ShopifyCLI
8
+ module Theme
9
+ class Syncer
10
+ class Uploader
11
+ class BulkJob < ShopifyCLI::ThreadPool::Job
12
+ INTERVAL = 0.2 # 200ms
13
+ MAX_RETRIES = 10
14
+
15
+ attr_reader :ctx, :bulk, :admin_api
16
+
17
+ def initialize(ctx, bulk)
18
+ super(INTERVAL)
19
+
20
+ @ctx = ctx
21
+ @bulk = bulk
22
+ @admin_api = bulk.admin_api
23
+
24
+ # Mutex used to coordinate changes in the bulk items
25
+ @bulk_item_mutex = Mutex.new
26
+
27
+ # Mutex used to coordinate changes in the bulk items
28
+ @bulk_item_mutex = Mutex.new
29
+ end
30
+
31
+ def perform!
32
+ bulk.wait_for_backoff!
33
+
34
+ # Fetch bulk items
35
+ bulk_items, bulk_size = bulk.consume_bulk_items
36
+ return if bulk_items.empty?
37
+
38
+ # Perform bulk request
39
+ log("job request: size=#{bulk_items.size}, bytesize=#{bulk_size}")
40
+ bulk_status, bulk_body, response = rest_request(bulk_items)
41
+ bulk.backoff_if_near_limit!(response)
42
+ log("job response: http_status=#{bulk_status}")
43
+
44
+ # Abort execution when a fatal error happens
45
+ return stable_flag_suggestion! if bulk_status != 207
46
+
47
+ # Handle item reponses
48
+ responses = parse_responses(bulk_body)
49
+ responses
50
+ .each_with_index do |tuple, index|
51
+ status, body = tuple
52
+ bulk_item = bulk_items[index]
53
+ handle_item_response(bulk_item, status, body, response)
54
+ end
55
+ ensure
56
+ bulk.clean_in_progress_items(bulk_items)
57
+ end
58
+
59
+ private
60
+
61
+ def handle_item_response(bulk_item, status, body, response)
62
+ if status == 200
63
+ return handle_success(bulk_item, status, body, response)
64
+ end
65
+
66
+ if bulk_item.retries < MAX_RETRIES
67
+ return handle_retry(bulk_item, status, body, response)
68
+ end
69
+
70
+ handle_error(bulk_item, status, body)
71
+ end
72
+
73
+ def handle_success(bulk_item, status, body, response)
74
+ log("bulk item success (item=#{bulk_item.key})")
75
+
76
+ @bulk_item_mutex.synchronize do
77
+ bulk_item.block.call(status, body, response)
78
+ end
79
+ end
80
+
81
+ def handle_retry(bulk_item, status, _body, _response)
82
+ key = bulk_item.key
83
+ retries = bulk_item.retries
84
+
85
+ log("bulk item error (item=#{key}, status=#{status}, retries=#{retries})")
86
+
87
+ @bulk_item_mutex.synchronize do
88
+ bulk_item.retries += 1
89
+ bulk.enqueue(bulk_item)
90
+ end
91
+ end
92
+
93
+ def handle_error(bulk_item, status, body)
94
+ log("bulk item fatal error (item=#{bulk_item.key}, status=#{status})")
95
+
96
+ @bulk_item_mutex.synchronize do
97
+ bulk_item.block.call(status, body, error_response(body))
98
+ end
99
+ end
100
+
101
+ def stable_flag_suggestion!
102
+ ctx.abort(ctx.message("theme.stable_flag_suggestion"))
103
+ end
104
+
105
+ def rest_request(bulk_items)
106
+ theme = bulk.theme
107
+ args = BulkRequest.new(theme, bulk_items).to_h
108
+
109
+ measure("bulk rest_request") { admin_api.rest_request(**args) }
110
+ end
111
+
112
+ def measure(subject)
113
+ return yield unless ctx.debug?
114
+
115
+ start_time = Time.now
116
+ result = yield
117
+ time_elapsed = (Time.now - start_time) * 1000
118
+
119
+ log("#{subject} time: #{time_elapsed}ms")
120
+
121
+ result
122
+ end
123
+
124
+ def parse_responses(body)
125
+ body["results"]&.map { |r| [r["code"], r["body"]] } || []
126
+ end
127
+
128
+ def error_response(body)
129
+ ShopifyCLI::API::APIRequestError.new(body, response: { body: body })
130
+ end
131
+
132
+ def log(message)
133
+ ctx.debug("[BulkJob ##{object_id}] #{message}")
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ class Uploader
7
+ class BulkRequest
8
+ def initialize(theme, bulk_items)
9
+ @theme = theme
10
+ @bulk_items = bulk_items
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ path: "themes/#{@theme.id}/assets/bulk.json",
16
+ method: "PUT",
17
+ body: JSON.generate({ assets: assets }),
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def assets
24
+ @bulk_items.map(&:asset)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "apply_to_all_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Uploader
9
+ module Forms
10
+ class ApplyToAll
11
+ attr_reader :value
12
+
13
+ def initialize(ctx, number_of_files)
14
+ @ctx = ctx
15
+ @number_of_files = number_of_files
16
+ @value = nil
17
+ @apply = nil
18
+ end
19
+
20
+ def apply?(value)
21
+ return unless @number_of_files > 1
22
+
23
+ if @apply.nil?
24
+ @apply = ask.apply?
25
+ @value = value if @apply
26
+ end
27
+
28
+ @apply
29
+ end
30
+
31
+ private
32
+
33
+ def ask
34
+ ApplyToAllForm.ask(@ctx, [], number_of_files: @number_of_files)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end