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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +1 -1
- data/lib/project_types/theme/commands/package.rb +20 -5
- data/lib/project_types/theme/messages/messages.rb +4 -2
- data/lib/shopify_cli/packager.rb +5 -14
- data/lib/shopify_cli/theme/backoff_helper.rb +47 -0
- data/lib/shopify_cli/theme/ignore_helper.rb +7 -1
- data/lib/shopify_cli/theme/syncer/downloader.rb +63 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk.rb +133 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk_item.rb +64 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk_job.rb +139 -0
- data/lib/shopify_cli/theme/syncer/uploader/bulk_request.rb +30 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all.rb +41 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/apply_to_all_form.rb +37 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/base_strategy_form.rb +64 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/select_delete_strategy.rb +29 -0
- data/lib/shopify_cli/theme/syncer/uploader/forms/select_update_strategy.rb +30 -0
- data/lib/shopify_cli/theme/syncer/uploader/json_delete_handler.rb +49 -0
- data/lib/shopify_cli/theme/syncer/uploader/json_update_handler.rb +71 -0
- data/lib/shopify_cli/theme/syncer/uploader.rb +227 -0
- data/lib/shopify_cli/theme/syncer.rb +91 -144
- data/lib/shopify_cli/version.rb +1 -1
- metadata +16 -16
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +0 -39
- data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +0 -35
- data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +0 -62
- data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +0 -27
- data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +0 -28
- data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +0 -51
- data/lib/shopify_cli/theme/syncer/json_update_handler.rb +0 -96
- data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk.rb +0 -102
- data/lib/shopify_cli/theme/theme_admin_api_throttler/bulk_job.rb +0 -75
- data/lib/shopify_cli/theme/theme_admin_api_throttler/errors.rb +0 -7
- data/lib/shopify_cli/theme/theme_admin_api_throttler/put_request.rb +0 -52
- data/lib/shopify_cli/theme/theme_admin_api_throttler/request_parser.rb +0 -39
- data/lib/shopify_cli/theme/theme_admin_api_throttler/response_parser.rb +0 -21
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3050c5be253ff04fbd3fa04ddd60389c0e6c0c2c0a3ae3fdcc6b059dc428c193
|
4
|
+
data.tar.gz: 2d373f27b01e11cd4df467114ba00849c978ae7cca1d8529749be5ba5d5d663d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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
|
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
|
37
|
-
|
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(
|
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: "
|
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
|
},
|
data/lib/shopify_cli/packager.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|