shopify-cli 2.29.0 → 2.30.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 +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
|