shopify-cli 2.15.1 → 2.15.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.vscode/settings.json +1 -2
  3. data/CHANGELOG.md +68 -20
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +21 -0
  6. data/ext/javy/hashes/javy-arm-macos-v0.3.0.gz.sha256 +1 -0
  7. data/ext/javy/hashes/javy-x86_64-linux-v0.3.0.gz.sha256 +1 -0
  8. data/ext/javy/hashes/javy-x86_64-macos-v0.3.0.gz.sha256 +1 -0
  9. data/ext/javy/hashes/javy-x86_64-windows-v0.3.0.gz.sha256 +1 -0
  10. data/ext/javy/version +1 -1
  11. data/ext/shopify-extensions/version +1 -1
  12. data/lib/project_types/extension/cli.rb +4 -0
  13. data/lib/project_types/extension/commands/check.rb +6 -1
  14. data/lib/project_types/extension/forms/questions/ask_template.rb +1 -2
  15. data/lib/project_types/extension/messages/messages.rb +1 -3
  16. data/lib/project_types/extension/models/development_server_requirements.rb +1 -0
  17. data/lib/project_types/extension/models/specification_handlers/beacon_extension.rb +57 -0
  18. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config.rb +33 -0
  19. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config_repository.rb +75 -0
  20. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +16 -1
  21. data/lib/project_types/extension/models/specification_handlers/theme_app_extension.rb +4 -1
  22. data/lib/project_types/extension/tasks/configure_options.rb +2 -1
  23. data/lib/project_types/extension/tasks/convert_server_config.rb +13 -2
  24. data/lib/project_types/extension/tasks/merge_server_config.rb +5 -2
  25. data/lib/project_types/script/cli.rb +1 -0
  26. data/lib/project_types/script/layers/application/create_script.rb +14 -6
  27. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  28. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +6 -21
  29. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  30. data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +35 -0
  31. data/lib/project_types/script/messages/messages.rb +3 -0
  32. data/lib/project_types/script/ui/error_handler.rb +11 -0
  33. data/lib/project_types/theme/cli.rb +1 -0
  34. data/lib/project_types/theme/commands/check.rb +4 -1
  35. data/lib/project_types/theme/commands/open.rb +2 -2
  36. data/lib/project_types/theme/commands/push.rb +1 -3
  37. data/lib/project_types/theme/commands/serve.rb +1 -0
  38. data/lib/project_types/theme/commands/share.rb +56 -0
  39. data/lib/project_types/theme/messages/messages.rb +64 -11
  40. data/lib/shopify_cli/changelog.rb +97 -25
  41. data/lib/shopify_cli/command_options/command_serve_options.rb +10 -0
  42. data/lib/shopify_cli/commands/app/serve.rb +7 -7
  43. data/lib/shopify_cli/commands/login.rb +5 -2
  44. data/lib/shopify_cli/context.rb +13 -0
  45. data/lib/shopify_cli/git.rb +36 -0
  46. data/lib/shopify_cli/identity_auth.rb +24 -4
  47. data/lib/shopify_cli/messages/messages.rb +22 -11
  48. data/lib/shopify_cli/release.rb +120 -20
  49. data/lib/shopify_cli/services/app/create/rails_service.rb +9 -1
  50. data/lib/shopify_cli/services/app/serve/node_service.rb +2 -25
  51. data/lib/shopify_cli/services/app/serve/php_service.rb +2 -25
  52. data/lib/shopify_cli/services/app/serve/rails_service.rb +8 -28
  53. data/lib/shopify_cli/services/app/serve/serve_service.rb +57 -0
  54. data/lib/shopify_cli/services.rb +1 -0
  55. data/lib/shopify_cli/tasks/update_dashboard_urls.rb +7 -9
  56. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  57. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  58. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  59. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  60. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  61. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +35 -0
  62. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  63. data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -8
  64. data/lib/shopify_cli/theme/dev_server.rb +18 -5
  65. data/lib/shopify_cli/theme/file.rb +15 -4
  66. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  67. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  68. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  69. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  70. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  71. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  72. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  73. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  74. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  75. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  76. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  77. data/lib/shopify_cli/theme/syncer.rb +79 -63
  78. data/lib/shopify_cli/theme/theme.rb +21 -7
  79. data/lib/shopify_cli/theme/theme_admin_api.rb +23 -8
  80. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  81. data/lib/shopify_cli/thread_pool.rb +15 -3
  82. data/lib/shopify_cli/tunnel.rb +3 -13
  83. data/lib/shopify_cli/version.rb +1 -1
  84. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +8 -0
  85. metadata +25 -2
@@ -23,19 +23,23 @@ module Script
23
23
  )
24
24
 
25
25
  # remove the need to pass the whole extension-point object to the infra layer
26
- sparse_checkout_repo = extension_point.libraries.for(language).repo
27
26
  type = extension_point.dasherize_type
28
27
  domain = extension_point.domain
29
28
 
29
+ sparse_checkout_details = Infrastructure::SparseCheckoutDetails.new(
30
+ repo: extension_point.libraries.for(language).repo,
31
+ branch: sparse_checkout_branch,
32
+ path: "#{domain}/#{language}/#{type}/default",
33
+ input_queries_enabled: input_queries_enabled?,
34
+ )
35
+
30
36
  project_creator = Infrastructure::Languages::ProjectCreator.for(
31
37
  ctx: ctx,
32
38
  language: language,
33
39
  type: type,
34
40
  project_name: title,
35
41
  path_to_project: project.id,
36
- sparse_checkout_repo: sparse_checkout_repo,
37
- sparse_checkout_branch: sparse_checkout_branch,
38
- sparse_checkout_set_path: "#{domain}/#{language}/#{type}/default"
42
+ sparse_checkout_details: sparse_checkout_details,
39
43
  )
40
44
 
41
45
  install_dependencies(ctx, language, title, project_creator)
@@ -49,12 +53,12 @@ module Script
49
53
  task_runner = Infrastructure::Languages::TaskRunner.for(ctx, language)
50
54
  CLI::UI::Frame.open(ctx.message(
51
55
  "core.git.pulling_from_to",
52
- project_creator.sparse_checkout_repo,
56
+ project_creator.sparse_checkout_details.repo,
53
57
  title,
54
58
  )) do
55
59
  UI::StrictSpinner.spin(ctx.message(
56
60
  "core.git.pulling",
57
- project_creator.sparse_checkout_repo,
61
+ project_creator.sparse_checkout_details.repo,
58
62
  title,
59
63
  )) do |spinner|
60
64
  project_creator.setup_dependencies
@@ -75,6 +79,10 @@ module Script
75
79
  ensure
76
80
  script_project_repo.change_to_initial_directory
77
81
  end
82
+
83
+ def input_queries_enabled?
84
+ ShopifyCLI::Feature.enabled?(:scripts_beta_input_queries)
85
+ end
78
86
  end
79
87
  end
80
88
  end
@@ -180,6 +180,23 @@ module Script
180
180
  @max_size = max_size
181
181
  end
182
182
  end
183
+
184
+ class InvalidInputQueryErrors < ScriptProjectError
185
+ attr_reader :messages
186
+
187
+ def initialize(messages)
188
+ @messages = messages
189
+ super()
190
+ end
191
+
192
+ def input_query_path
193
+ ScriptProjectRepository::INPUT_QUERY_PATH
194
+ end
195
+
196
+ def message
197
+ messages.join("\n")
198
+ end
199
+ end
183
200
  end
184
201
  end
185
202
  end
@@ -10,9 +10,7 @@ module Script
10
10
  property! :type, accepts: String
11
11
  property! :project_name, accepts: String
12
12
  property! :path_to_project, accepts: String
13
- property! :sparse_checkout_repo, accepts: String
14
- property! :sparse_checkout_branch, accepts: String
15
- property! :sparse_checkout_set_path, accepts: String
13
+ property! :sparse_checkout_details, accepts: SparseCheckoutDetails
16
14
 
17
15
  def self.for(
18
16
  ctx:,
@@ -20,9 +18,7 @@ module Script
20
18
  type:,
21
19
  project_name:,
22
20
  path_to_project:,
23
- sparse_checkout_repo:,
24
- sparse_checkout_branch:,
25
- sparse_checkout_set_path:
21
+ sparse_checkout_details:
26
22
  )
27
23
 
28
24
  project_creators = {
@@ -36,33 +32,22 @@ module Script
36
32
  type: type,
37
33
  project_name: project_name,
38
34
  path_to_project: path_to_project,
39
- sparse_checkout_repo: sparse_checkout_repo,
40
- sparse_checkout_branch: sparse_checkout_branch,
41
- sparse_checkout_set_path: sparse_checkout_set_path
35
+ sparse_checkout_details: sparse_checkout_details,
42
36
  )
43
37
  end
44
38
 
45
39
  # the sparse checkout process is common to all script types
46
40
  def setup_dependencies
47
- setup_sparse_checkout
41
+ sparse_checkout_details.setup(ctx)
48
42
  clean
49
43
  end
50
44
 
51
45
  private
52
46
 
53
- def setup_sparse_checkout
54
- ShopifyCLI::Git.sparse_checkout(
55
- sparse_checkout_repo,
56
- sparse_checkout_set_path,
57
- sparse_checkout_branch,
58
- ctx
59
- )
60
- end
61
-
62
47
  def clean
63
- source = File.join(path_to_project, sparse_checkout_set_path, ".")
48
+ source = File.join(path_to_project, sparse_checkout_details.path, ".")
64
49
  FileUtils.cp_r(source, path_to_project)
65
- ctx.rm_rf(sparse_checkout_set_path.split("/")[0])
50
+ ctx.rm_rf(sparse_checkout_details.path.split("/")[0])
66
51
  ctx.rm_rf(".git")
67
52
  end
68
53
 
@@ -83,6 +83,8 @@ module Script
83
83
  valid_types: e["message"],
84
84
  filename: script_config.filename,
85
85
  )
86
+ elsif (errors = user_errors.filter { |err| err["tag"] == "input_query_validation_error" }).any?
87
+ raise Errors::InvalidInputQueryErrors, errors.map { |err| err["message"] }
86
88
  elsif user_errors.find { |err| %w(not_use_msgpack_error schema_version_argument_error).include?(err["tag"]) }
87
89
  raise Domain::Errors::MetadataValidationError
88
90
  else
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Script
4
+ module Layers
5
+ module Infrastructure
6
+ class SparseCheckoutDetails
7
+ include SmartProperties
8
+ property! :repo, accepts: String
9
+ property! :branch, accepts: String
10
+ property! :path, accepts: String
11
+ property! :input_queries_enabled, accepts: [true, false]
12
+
13
+ def ==(other)
14
+ self.class == other.class &&
15
+ self.class.properties.all? { |name, _| self[name] == other[name] }
16
+ end
17
+
18
+ def setup(ctx)
19
+ ShopifyCLI::Git.sparse_checkout(repo, patterns_to_checkout, branch, ctx)
20
+ end
21
+
22
+ private
23
+
24
+ def patterns_to_checkout
25
+ paths = [path]
26
+ unless input_queries_enabled
27
+ paths << "!#{path}/#{ScriptProjectRepository::INPUT_QUERY_PATH}"
28
+ paths << "!#{path}/schema.graphql"
29
+ end
30
+ paths.join(" ")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -86,6 +86,9 @@ module Script
86
86
  "type(s): %{valid_types}.",
87
87
  configuration_schema_field_invalid_value_error_help: "Change the value of the type.",
88
88
 
89
+ input_query_error_cause: "Input query is invalid:\n%{messages}\n\n",
90
+ input_query_error_help: "Fix the query in the `%{input_query_path}` file.",
91
+
89
92
  script_not_found_cause: "Can't find script %s for Script API %s",
90
93
 
91
94
  system_call_failure_cause: "Something went wrong while running: {{command:%{cmd}}}.",
@@ -216,6 +216,17 @@ module Script
216
216
  "script.error.configuration_schema_field_invalid_value_error_help"
217
217
  ),
218
218
  }
219
+ when Layers::Infrastructure::Errors::InvalidInputQueryErrors
220
+ {
221
+ cause_of_error: ShopifyCLI::Context.message(
222
+ "script.error.input_query_error_cause",
223
+ messages: e.messages.map { |message| " {{x}} #{message}." }.join("\n"),
224
+ ),
225
+ help_suggestion: ShopifyCLI::Context.message(
226
+ "script.error.input_query_error_help",
227
+ input_query_path: e.input_query_path,
228
+ ),
229
+ }
219
230
  when Layers::Infrastructure::Errors::DependencyInstallError
220
231
  {
221
232
  cause_of_error: ShopifyCLI::Context.message("script.error.dependency_install_cause"),
@@ -16,6 +16,7 @@ module Theme
16
16
  subcommand :Package, "package", Project.project_filepath("commands/package")
17
17
  subcommand :Open, "open", Project.project_filepath("commands/open")
18
18
  subcommand :List, "list", Project.project_filepath("commands/list")
19
+ subcommand :Share, "share", Project.project_filepath("commands/share")
19
20
  subcommand :LanguageServer, "language-server", Project.project_filepath("commands/language_server")
20
21
  end
21
22
  ShopifyCLI::Commands.register("Theme::Command", "theme")
@@ -24,7 +24,10 @@ module Theme
24
24
  end
25
25
 
26
26
  def call(*)
27
- @theme_check.run
27
+ @theme_check.run!
28
+ rescue ThemeCheck::Cli::Abort, ThemeCheck::ThemeCheckError => e
29
+ raise ShopifyCLI::Abort,
30
+ ShopifyCLI::Context.message("theme.check.error", e.full_message)
28
31
  end
29
32
 
30
33
  def self.help
@@ -17,8 +17,8 @@ module Theme
17
17
  def call(_args, _name)
18
18
  theme = find_theme(**options.flags)
19
19
 
20
- @ctx.puts(@ctx.message("theme.open.details", theme.name, theme.editor_url))
21
- @ctx.open_url!(theme.preview_url)
20
+ @ctx.puts(@ctx.message("theme.open.details", theme.name, theme.preview_url, theme.editor_url))
21
+ @ctx.open_browser_url!(theme.preview_url)
22
22
  end
23
23
 
24
24
  def self.help
@@ -104,9 +104,7 @@ module Theme
104
104
 
105
105
  if unpublished
106
106
  name = theme || ask_theme_name
107
- new_theme = ShopifyCLI::Theme::Theme.new(@ctx, root: root, name: name, role: "unpublished")
108
- new_theme.create
109
- return new_theme
107
+ return ShopifyCLI::Theme::Theme.create_unpublished(@ctx, root: root, name: name)
110
108
  end
111
109
 
112
110
  if theme
@@ -16,6 +16,7 @@ module Theme
16
16
  parser.on("--port=PORT") { |port| flags[:port] = port.to_i }
17
17
  parser.on("--poll") { flags[:poll] = true }
18
18
  parser.on("--live-reload=MODE") { |mode| flags[:mode] = as_reload_mode(mode) }
19
+ parser.on("--theme-editor-sync") { flags[:editor_sync] = true }
19
20
  end
20
21
 
21
22
  def call(_args, name)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/theme/theme"
4
+ require "shopify_cli/theme/syncer"
5
+ require "project_types/theme/commands/common/root_helper"
6
+
7
+ module Theme
8
+ class Command
9
+ class Share < ShopifyCLI::Command::SubCommand
10
+ include Common::RootHelper
11
+
12
+ recommend_default_ruby_range
13
+
14
+ def call(_args, name)
15
+ root = root_value(options, name)
16
+ theme = create_theme(root)
17
+
18
+ upload(theme)
19
+
20
+ @ctx.done(done_message(theme))
21
+ end
22
+
23
+ def self.help
24
+ tool = ShopifyCLI::TOOL_NAME
25
+ @ctx.message("theme.share.help", tool, tool)
26
+ end
27
+
28
+ private
29
+
30
+ def create_theme(root)
31
+ ShopifyCLI::Theme::Theme.create_unpublished(@ctx, root: root)
32
+ end
33
+
34
+ def upload(theme)
35
+ syncer = ShopifyCLI::Theme::Syncer.new(@ctx, theme: theme)
36
+ syncer.start_threads
37
+
38
+ CLI::UI::Frame.open(upload_message(theme)) do
39
+ UI::SyncProgressBar.new(syncer).progress(:upload_theme!)
40
+ end
41
+
42
+ raise ShopifyCLI::AbortSilent if syncer.has_any_error?
43
+ ensure
44
+ syncer.shutdown
45
+ end
46
+
47
+ def upload_message(theme)
48
+ @ctx.message("theme.share.upload", theme.name, theme.id, theme.shop)
49
+ end
50
+
51
+ def done_message(theme)
52
+ @ctx.message("theme.share.done", theme.name, theme.preview_url, theme.editor_url)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -8,8 +8,9 @@ module Theme
8
8
  Usage: {{command:%1$s theme [ %2$s ]}}
9
9
  HELP
10
10
  ensure_user_error: "You are not authorized to edit themes on %s.",
11
- ensure_user_try_this: "Make sure you are a user of that store, and allowed to edit themes.",
12
-
11
+ ensure_user_try_this: <<~ENSURE_USER,
12
+ Check if your user is activated, has permission to edit themes at the store, and try to re-login.
13
+ ENSURE_USER
13
14
  init: {
14
15
  help: <<~HELP,
15
16
  {{command:%s theme init}}: Clones a Git repository to use as a starting point for building a new theme.
@@ -101,13 +102,14 @@ module Theme
101
102
  Usage: {{command:%s theme serve [ ROOT ]}}
102
103
 
103
104
  Options:
104
- {{command:--port=PORT}} Local port to serve theme preview from.
105
- {{command:--poll}} Force polling to detect file changes.
106
- {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
107
- {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
108
- - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
109
- - {{command:full-page}} Always refreshes the entire page
110
- - {{command:off}} Deactivate live reload
105
+ {{command:--port=PORT}} Local port to serve theme preview from.
106
+ {{command:--poll}} Force polling to detect file changes.
107
+ {{command:--host=HOST}} Set which network interface the web server listens on. The default value is 127.0.0.1.
108
+ {{command:--theme-editor-sync}} Synchronize Theme Editor updates in the local theme files.
109
+ {{command:--live-reload=MODE}} The live reload mode switches the server behavior when a file is modified:
110
+ - {{command:hot-reload}} Hot reloads local changes to CSS and sections (default)
111
+ - {{command:full-page}} Always refreshes the entire page
112
+ - {{command:off}} Deactivate live reload
111
113
  HELP
112
114
  reload_mode_is_not_valid: "The live reload mode `%s` is not valid.",
113
115
  try_a_valid_reload_mode: "Try a valid live reload mode: %s.",
@@ -121,6 +123,36 @@ module Theme
121
123
  fixed: "Fixed",
122
124
  },
123
125
  },
126
+ syncer: {
127
+ forms: {
128
+ apply_to_all: {
129
+ title: "Would like apply this to all the other %s files?",
130
+ yes: "Yes",
131
+ no: "No",
132
+ },
133
+ update_strategy: {
134
+ title_context: <<~TITLE,
135
+
136
+ The local file {{command:%s}} is different from the remote version in the development theme."
137
+ TITLE
138
+ title_question: "What would you like to do?",
139
+ keep_remote: "Keep the remote version",
140
+ keep_local: "Keep the local version",
141
+ union_merge: "Merge files (it may break the local file)",
142
+ exit: "Exit",
143
+ },
144
+ delete_strategy: {
145
+ title_context: <<~TITLE,
146
+
147
+ The local file {{command:%s}} has been recently removed, but it's present on your remote development theme.",
148
+ TITLE
149
+ title_question: "What would you like to do?",
150
+ delete: "Delete permanently",
151
+ restore: "Restore with the remote version",
152
+ exit: "Exit",
153
+ },
154
+ },
155
+ },
124
156
  error: {
125
157
  address_binding_error: "Couldn't bind to localhost."\
126
158
  " To serve your theme, set a different address with {{command:%s theme serve --host=<address>}}",
@@ -137,9 +169,10 @@ module Theme
137
169
  Serving %s
138
170
 
139
171
  SERVING
172
+ download_changes: ", and use 'theme pull' to get the changes",
140
173
  customize_or_preview: <<~CUSTOMIZE_OR_PREVIEW,
141
174
 
142
- Customize this theme in the Theme Editor:
175
+ Customize this theme in the Theme Editor%s:
143
176
  {{green:%s}}
144
177
 
145
178
  Share this theme preview:
@@ -149,7 +182,7 @@ module Theme
149
182
  CUSTOMIZE_OR_PREVIEW
150
183
  ensure_user: <<~ENSURE_USER,
151
184
  You are not authorized to edit themes on %s.
152
- Make sure you are a user of that store, and allowed to edit themes.
185
+ Check if your user is activated, has permission to edit themes at the store, and try to re-login.
153
186
  ENSURE_USER
154
187
  address_already_in_use: "The address \"%s\" is already in use.",
155
188
  try_port_option: "Use the --port=PORT option to serve the theme in a different port.",
@@ -159,6 +192,7 @@ module Theme
159
192
  Check your theme for errors, suggestions, and best practices.
160
193
  Usage: {{command:%s check}}
161
194
  HELP
195
+ error: "Theme check failed with error:\n%s",
162
196
  },
163
197
  delete: {
164
198
  help: <<~HELP,
@@ -230,6 +264,9 @@ module Theme
230
264
  details: <<~DETAILS,
231
265
  {{*}} {{bold:%s}}
232
266
 
267
+ Preview your theme:
268
+ {{green:%s}}
269
+
233
270
  Customize your theme in the Theme Editor:
234
271
  {{green:%s}}
235
272
 
@@ -253,6 +290,22 @@ module Theme
253
290
  Usage: {{command:%s theme list}}
254
291
  HELP
255
292
  },
293
+ share: {
294
+ help: <<~HELP,
295
+ {{command:%s theme share}}: Creates a shareable, unpublished, and new theme on your theme library with a randomized name.
296
+ Works like an alias to {{command:theme push -u -t=RANDOMIZED_NAME}}.
297
+
298
+ Usage: {{command:%s theme share [ ROOT ]}}
299
+ HELP
300
+ done: <<~DONE,
301
+ {{green:The {{bold:%s}} theme was pushed successfully}}
302
+
303
+ {{info:Share your theme preview:}}
304
+ {{underline:%s}}
305
+
306
+ DONE
307
+ upload: "Pushing theme files to %s (#%s) on %s",
308
+ },
256
309
  },
257
310
  }.freeze
258
311
  end
@@ -1,23 +1,32 @@
1
+ require "date"
1
2
  require "shopify_cli/sed"
3
+ require "octokit"
2
4
 
3
5
  module ShopifyCLI
4
6
  class Changelog
5
7
  CHANGELOG_FILE = File.join(ShopifyCLI::ROOT, "CHANGELOG.md")
8
+ CHANGE_CATEGORIES = %w(Added Changed Deprecated Removed Fixed Security)
6
9
 
7
10
  def initialize
8
11
  load(File.read(CHANGELOG_FILE))
9
12
  end
10
13
 
11
14
  def update_version!(new_version)
12
- Sed.new.replace_inline(
13
- CHANGELOG_FILE,
14
- "## \\[Unreleased\\]",
15
- "## [Unreleased]\\n\\n## Version #{new_version}"
16
- )
15
+ changes[new_version] = changes["Unreleased"]
16
+ changes[new_version][:date] = Date.today.iso8601
17
+ changes["Unreleased"] = { changes: [], date: nil }
18
+ save!
19
+ end
20
+
21
+ def update!
22
+ pr = pr_for_current_branch
23
+ category = CLI::UI::Prompt.ask("What type of change?", options: CHANGE_CATEGORIES)
24
+ add_change(category, { pr_id: pr.number, desc: pr.title })
25
+ save!
17
26
  end
18
27
 
19
28
  def release_notes(version)
20
- changes[version].map do |change_category, changes|
29
+ changes[version][:changes].map do |change_category, changes|
21
30
  <<~CHANGES
22
31
  ### #{change_category}
23
32
  #{changes.map { |change| entry(**change) }.join("\n")}
@@ -25,17 +34,61 @@ module ShopifyCLI
25
34
  end.join("\n")
26
35
  end
27
36
 
37
+ def add_change(category, change)
38
+ changes["Unreleased"][:changes][category] << change
39
+ end
40
+
28
41
  def entry(pr_id:, desc:)
29
42
  "* [##{pr_id}](https://github.com/Shopify/shopify-cli/pull/#{pr_id}): #{desc}"
30
43
  end
31
44
 
45
+ def full_contents
46
+ sorted_changes = changes.each_key.sort_by do |change|
47
+ if change == "Unreleased"
48
+ [Float::INFINITY] * 3 # end of the list
49
+ else
50
+ major, minor, patch = change.split(".").map(&:to_i)
51
+ [major, minor, patch]
52
+ end
53
+ end.reverse
54
+ [
55
+ heading,
56
+ *sorted_changes.each.map { |version| release_notes_with_header(version) }.join,
57
+ remainder,
58
+ ].map { |section| section.chomp << "\n" }.join
59
+ end
60
+
61
+ def save!
62
+ File.write(CHANGELOG_FILE, full_contents)
63
+ end
64
+
32
65
  private
33
66
 
67
+ attr_reader :heading, :remainder
68
+
69
+ def release_notes_with_header(version)
70
+ header_line =
71
+ if version == "Unreleased"
72
+ "[Unreleased]"
73
+ else
74
+ date = changes[version][:date]
75
+ "Version #{version}#{" - #{date}" if date}"
76
+ end
77
+
78
+ [
79
+ "## #{header_line}",
80
+ release_notes(version),
81
+ ].reject(&:empty?).map { |section| section.chomp << "\n\n" }.join
82
+ end
83
+
34
84
  def changes
35
85
  @changes ||= Hash.new do |h, k|
36
- h[k] = Hash.new do |h2, k2|
37
- h2[k2] = []
38
- end
86
+ h[k] = {
87
+ date: nil,
88
+ changes: Hash.new do |h2, k2|
89
+ h2[k2] = []
90
+ end,
91
+ }
39
92
  end
40
93
  end
41
94
 
@@ -43,34 +96,53 @@ module ShopifyCLI
43
96
  state = :initial
44
97
  change_category = nil
45
98
  current_version = nil
99
+ @heading = ""
46
100
  @remainder = ""
47
101
  log.each_line do |line|
48
102
  case state
49
103
  when :initial
50
- next unless line.chomp == "\#\# [Unreleased]"
51
- state = :unreleased
52
- current_version = "Unreleased"
53
- when :unreleased, :last_version
104
+ if line.chomp == "\#\# [Unreleased]"
105
+ state = :unreleased
106
+ current_version = "Unreleased"
107
+ # Ensure Unreleased changeset exists even if no changes have happened yet
108
+ changes["Unreleased"]
109
+ else
110
+ @heading << line
111
+ end
112
+ when :unreleased, :prior_versions
54
113
  if /\A\#\#\# (?<category>\w+)/ =~ line
55
114
  change_category = category
56
- elsif %r{\A\* \[\#(?<pr_id>\d+)\]\(https://github.com/Shopify/shopify-cli/pull/\d+\): (?<desc>.+)\n} =~ line
57
- changes[current_version][change_category] << { pr_id: pr_id, desc: desc }
58
- elsif /\A\#\# Version (?<version>\d+\.\d+\.\d+)/ =~ line
115
+ elsif %r{\A\* \[\#(?<id>\d+)\]\(https://github.com/Shopify/shopify-cli/pull/\k<id>\): (?<desc>.+)\n} =~ line
116
+ changes[current_version][:changes][change_category] << { pr_id: id, desc: desc }
117
+ elsif /\A\#\# Version (?<version>\d+\.\d+\.\d+)( - (?<date>\d{4}-\d{2}-\d{2}))?/ =~ line
59
118
  current_version = version
60
- state =
61
- case state
62
- when :unreleased
63
- :last_version
64
- else
65
- :finished
66
- end
119
+ state = :prior_versions
120
+ major, minor, _patch = current_version.split(".")
121
+ if major.to_i <= 2 && minor.to_i < 7
122
+ # Changelog starts to become irregular in 2.6.x
123
+ state = :finished
124
+ end
125
+ changes[current_version][:date] = date unless state == :finished
67
126
  elsif !line.match?(/\s*\n/)
68
127
  raise "Unrecognized line: #{line.inspect}"
69
128
  end
70
- when :finished
71
- @remainder << line
72
129
  end
130
+ @remainder << line if state == :finished
73
131
  end
74
132
  end
133
+
134
+ def pr_for_current_branch
135
+ current_branch = %x(git branch --show-current).chomp
136
+ search_term = "repo:Shopify/shopify-cli is:pr is:open head:#{current_branch}"
137
+ results = Octokit::Client.new.search_issues(search_term)
138
+ case results.total_count
139
+ when 0
140
+ raise "PR not opened yet!"
141
+ when (2..)
142
+ raise "Multiple open PRs, not sure which one to use for changelog!"
143
+ end
144
+
145
+ results.items.first
146
+ end
75
147
  end
76
148
  end
@@ -20,6 +20,10 @@ module ShopifyCLI
20
20
  end
21
21
  host
22
22
  end
23
+
24
+ def no_update
25
+ options.flags[:no_update] || false
26
+ end
23
27
  end
24
28
  end
25
29
 
@@ -37,6 +41,12 @@ module ShopifyCLI
37
41
  parser.on("--port=PORT") { |port| flags[:port] = port }
38
42
  end
39
43
  end
44
+
45
+ def parse_no_update_option
46
+ options do |parser, flags|
47
+ parser.on("--no-update") { flags[:no_update] = true }
48
+ end
49
+ end
40
50
  end
41
51
  end
42
52
  end