shopify-cli 2.13.0 → 2.15.1

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +5 -0
  3. data/.github/CONTRIBUTING.md +1 -1
  4. data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  5. data/.github/workflows/stale.yml +46 -0
  6. data/CHANGELOG.md +36 -0
  7. data/Gemfile +1 -0
  8. data/Gemfile.lock +43 -11
  9. data/Rakefile +43 -0
  10. data/ext/javy/hashes/javy-arm-macos-v0.2.1.gz.sha256 +1 -0
  11. data/ext/javy/hashes/javy-x86_64-linux-v0.2.1.gz.sha256 +1 -0
  12. data/ext/javy/hashes/javy-x86_64-macos-v0.2.1.gz.sha256 +1 -0
  13. data/ext/javy/hashes/javy-x86_64-windows-v0.2.1.gz.sha256 +1 -0
  14. data/ext/javy/version +1 -1
  15. data/ext/shopify-extensions/version +1 -1
  16. data/lib/project_types/extension/forms/questions/ask_template.rb +5 -8
  17. data/lib/project_types/extension/messages/messages.rb +10 -0
  18. data/lib/project_types/extension/models/development_server_requirements.rb +13 -7
  19. data/lib/project_types/extension/models/npm_package.rb +19 -1
  20. data/lib/project_types/extension/models/server_config/development_renderer.rb +4 -3
  21. data/lib/project_types/extension/models/server_config/root.rb +2 -0
  22. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +13 -0
  23. data/lib/project_types/script/cli.rb +0 -4
  24. data/lib/project_types/script/config/extension_points.yml +18 -6
  25. data/lib/project_types/script/layers/application/build_script.rb +3 -18
  26. data/lib/project_types/script/layers/application/push_script.rb +12 -10
  27. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +0 -1
  28. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +0 -1
  29. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +2 -10
  30. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +1 -1
  31. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +1 -23
  32. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +1 -1
  33. data/lib/project_types/script/messages/messages.rb +2 -2
  34. data/lib/project_types/theme/commands/package.rb +1 -0
  35. data/lib/project_types/theme/commands/pull.rb +2 -2
  36. data/lib/project_types/theme/commands/push.rb +2 -2
  37. data/lib/project_types/theme/conversions/base_glob.rb +20 -5
  38. data/lib/project_types/theme/messages/messages.rb +9 -0
  39. data/lib/shopify_cli/changelog.rb +76 -0
  40. data/lib/shopify_cli/command.rb +8 -7
  41. data/lib/shopify_cli/commands/app/deploy.rb +0 -1
  42. data/lib/shopify_cli/context.rb +2 -2
  43. data/lib/shopify_cli/core/entry_point.rb +1 -1
  44. data/lib/shopify_cli/core/monorail.rb +14 -6
  45. data/lib/shopify_cli/environment.rb +19 -11
  46. data/lib/shopify_cli/exception_reporter.rb +2 -0
  47. data/lib/shopify_cli/messages/messages.rb +11 -5
  48. data/lib/shopify_cli/packager.rb +1 -1
  49. data/lib/shopify_cli/release.rb +94 -0
  50. data/lib/shopify_cli/result.rb +14 -0
  51. data/lib/shopify_cli/sed.rb +19 -0
  52. data/lib/shopify_cli/services/app/create/node_service.rb +2 -14
  53. data/lib/shopify_cli/services/app/create/php_service.rb +1 -6
  54. data/lib/shopify_cli/services/app/create/rails_service.rb +4 -12
  55. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +5 -5
  56. data/lib/shopify_cli/theme/dev_server/watcher.rb +10 -2
  57. data/lib/shopify_cli/theme/development_theme.rb +2 -5
  58. data/lib/shopify_cli/theme/syncer.rb +20 -25
  59. data/lib/shopify_cli/theme/theme.rb +28 -31
  60. data/lib/shopify_cli/theme/theme_admin_api.rb +72 -0
  61. data/lib/shopify_cli/transform_data_structure.rb +3 -2
  62. data/lib/shopify_cli/tunnel.rb +9 -0
  63. data/lib/shopify_cli/version.rb +1 -1
  64. data/shipit.yml +3 -0
  65. data/shopify-cli.gemspec +12 -3
  66. metadata +18 -10
  67. data/lib/project_types/rails/ruby.rb +0 -17
  68. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_project_creator.rb +0 -36
  69. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +0 -109
@@ -26,7 +26,6 @@ module Script
26
26
  )
27
27
 
28
28
  project_creators = {
29
- "assemblyscript" => AssemblyScriptProjectCreator,
30
29
  "typescript" => TypeScriptProjectCreator,
31
30
  "wasm" => WasmProjectCreator,
32
31
  }
@@ -9,7 +9,6 @@ module Script
9
9
 
10
10
  def self.for(ctx, language)
11
11
  task_runners = {
12
- "assemblyscript" => AssemblyScriptTaskRunner,
13
12
  "typescript" => TypeScriptTaskRunner,
14
13
  "wasm" => WasmTaskRunner,
15
14
  }
@@ -18,7 +18,8 @@ module Script
18
18
 
19
19
  def build
20
20
  compile
21
- bytecode
21
+ rescue Errors::SystemCallFailureError => e
22
+ raise Errors::BuildError, e.out
22
23
  end
23
24
 
24
25
  def install_dependencies
@@ -95,15 +96,6 @@ module Script
95
96
  raise Errors::BuildScriptNotFoundError,
96
97
  "Build script not found" if build_script.nil?
97
98
  end
98
-
99
- def bytecode
100
- raise Errors::WebAssemblyBinaryNotFoundError unless ctx.file_exist?(BYTECODE_FILE)
101
-
102
- contents = ctx.binread(BYTECODE_FILE)
103
- ctx.rm(BYTECODE_FILE)
104
-
105
- contents
106
- end
107
99
  end
108
100
  end
109
101
  end
@@ -5,7 +5,7 @@ module Script
5
5
  module Infrastructure
6
6
  module Languages
7
7
  class WasmTaskRunner < TaskRunner
8
- BYTECODE_FILE = "script.wasm"
8
+ BYTECODE_FILE = "build/index.wasm"
9
9
 
10
10
  def dependencies_installed?
11
11
  true
@@ -7,23 +7,6 @@ module Script
7
7
  include SmartProperties
8
8
  property! :ctx, accepts: ShopifyCLI::Context
9
9
 
10
- def create_push_package(script_project:, script_content:, metadata:, library:)
11
- build_file_path = file_path(script_project.id)
12
- write_to_path(build_file_path, script_content)
13
-
14
- Domain::PushPackage.new(
15
- id: build_file_path,
16
- uuid: script_project.uuid,
17
- extension_point_type: script_project.extension_point_type,
18
- title: script_project.title,
19
- description: script_project.description,
20
- script_content: script_content,
21
- metadata: metadata,
22
- script_config: script_project.script_config,
23
- library: library
24
- )
25
- end
26
-
27
10
  def get_push_package(script_project:, metadata:, library:)
28
11
  build_file_path = file_path(script_project.id)
29
12
  raise Domain::Errors::PushPackageNotFoundError unless ctx.file_exist?(build_file_path)
@@ -44,13 +27,8 @@ module Script
44
27
 
45
28
  private
46
29
 
47
- def write_to_path(path, content)
48
- ctx.mkdir_p(File.dirname(path))
49
- ctx.binwrite(path, content)
50
- end
51
-
52
30
  def file_path(path_to_script)
53
- "#{path_to_script}/build/script.wasm"
31
+ "#{path_to_script}/build/index.wasm"
54
32
  end
55
33
  end
56
34
  end
@@ -136,7 +136,7 @@ module Script
136
136
  end
137
137
 
138
138
  def default_language
139
- "assemblyscript"
139
+ "wasm"
140
140
  end
141
141
 
142
142
  def validate_metadata!(extension_point_type, language)
@@ -135,8 +135,8 @@ module Script
135
135
  "\nbuild: npx shopify-scripts-toolchain-as build --src src/shopify_main.ts --binary build/<script_name>.wasm --metadata build/metadata.json -- --lib node_modules --optimize --use Date=",
136
136
 
137
137
  web_assembly_binary_not_found: "Wasm binary not found.",
138
- web_assembly_binary_not_found_suggestion: "Check that there is a valid Wasm binary in the root directory" \
139
- "Your Wasm binary should match the script name: <script_name>.wasm",
138
+ web_assembly_binary_not_found_suggestion: "Check that a valid Wasm binary is present. " \
139
+ "The Wasm binary's filepath must be 'build/index.wasm'.",
140
140
 
141
141
  project_config_not_found: "Internal error - Script can't be created because the project's config file is missing from the repository.",
142
142
 
@@ -15,6 +15,7 @@ module Theme
15
15
  sections
16
16
  snippets
17
17
  templates
18
+ release-notes.md
18
19
  ]
19
20
 
20
21
  def call(args, _name)
@@ -26,11 +26,11 @@ module Theme
26
26
  parser.on("-d", "--development") { flags[:development] = true }
27
27
  parser.on("-o", "--only=PATTERN", Conversions::IncludeGlob) do |pattern|
28
28
  flags[:includes] ||= []
29
- flags[:includes] += pattern
29
+ flags[:includes] |= pattern
30
30
  end
31
31
  parser.on("-x", "--ignore=PATTERN", Conversions::IgnoreGlob) do |pattern|
32
32
  flags[:ignores] ||= []
33
- flags[:ignores] += pattern
33
+ flags[:ignores] |= pattern
34
34
  end
35
35
  end
36
36
 
@@ -30,11 +30,11 @@ module Theme
30
30
  parser.on("-p", "--publish") { flags[:publish] = true }
31
31
  parser.on("-o", "--only=PATTERN", Conversions::IncludeGlob) do |pattern|
32
32
  flags[:includes] ||= []
33
- flags[:includes] += pattern
33
+ flags[:includes] |= pattern
34
34
  end
35
35
  parser.on("-x", "--ignore=PATTERN", Conversions::IgnoreGlob) do |pattern|
36
36
  flags[:ignores] ||= []
37
- flags[:ignores] += pattern
37
+ flags[:ignores] |= pattern
38
38
  end
39
39
  end
40
40
 
@@ -10,8 +10,22 @@ module Theme
10
10
 
11
11
  def convert(parser)
12
12
  argv = parser.default_argv
13
- option_index = argv.index { |v| options.include?(v) }
13
+ values = []
14
+
15
+ option_indexes(argv).each do |option_index|
16
+ values += option_values(argv, parser, option_index)
17
+ end
18
+
19
+ values
20
+ end
21
+
22
+ def options
23
+ raise "`#{self.class.name}#options` must be defined"
24
+ end
25
+
26
+ private
14
27
 
28
+ def option_values(argv, parser, option_index)
15
29
  return [] if option_index.nil?
16
30
 
17
31
  start_index = option_index + 1
@@ -26,12 +40,13 @@ module Theme
26
40
  values
27
41
  end
28
42
 
29
- def options
30
- raise "`#{self.class.name}#options` must be defined"
43
+ def option_indexes(argv)
44
+ argv
45
+ .each_with_index
46
+ .select { |item, _index| options.include?(item) }
47
+ .map(&:last)
31
48
  end
32
49
 
33
- private
34
-
35
50
  def options_map(parser)
36
51
  map = {}
37
52
  parser.top.list.each do |option|
@@ -7,6 +7,8 @@ module Theme
7
7
  Suite of commands for developing Shopify themes. See {{command:%1$s theme <command> --help}} for usage of each command.
8
8
  Usage: {{command:%1$s theme [ %2$s ]}}
9
9
  HELP
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.",
10
12
 
11
13
  init: {
12
14
  help: <<~HELP,
@@ -122,6 +124,13 @@ module Theme
122
124
  error: {
123
125
  address_binding_error: "Couldn't bind to localhost."\
124
126
  " To serve your theme, set a different address with {{command:%s theme serve --host=<address>}}",
127
+ invalid_subdirectory: <<~MESSAGE,
128
+ The presence of %s in the directory structure isn't supported.
129
+
130
+ Move any files to a parent folder, then delete unsupported subdirectories.
131
+
132
+ • Required directory structure: https://shopify.dev/themes/architecture#directory-structure-and-component-types
133
+ MESSAGE
125
134
  },
126
135
  serving: <<~SERVING,
127
136
 
@@ -0,0 +1,76 @@
1
+ require "shopify_cli/sed"
2
+
3
+ module ShopifyCLI
4
+ class Changelog
5
+ CHANGELOG_FILE = File.join(ShopifyCLI::ROOT, "CHANGELOG.md")
6
+
7
+ def initialize
8
+ load(File.read(CHANGELOG_FILE))
9
+ end
10
+
11
+ def update_version!(new_version)
12
+ Sed.new.replace_inline(
13
+ CHANGELOG_FILE,
14
+ "## \\[Unreleased\\]",
15
+ "## [Unreleased]\\n\\n## Version #{new_version}"
16
+ )
17
+ end
18
+
19
+ def release_notes(version)
20
+ changes[version].map do |change_category, changes|
21
+ <<~CHANGES
22
+ ### #{change_category}
23
+ #{changes.map { |change| entry(**change) }.join("\n")}
24
+ CHANGES
25
+ end.join("\n")
26
+ end
27
+
28
+ def entry(pr_id:, desc:)
29
+ "* [##{pr_id}](https://github.com/Shopify/shopify-cli/pull/#{pr_id}): #{desc}"
30
+ end
31
+
32
+ private
33
+
34
+ def changes
35
+ @changes ||= Hash.new do |h, k|
36
+ h[k] = Hash.new do |h2, k2|
37
+ h2[k2] = []
38
+ end
39
+ end
40
+ end
41
+
42
+ def load(log)
43
+ state = :initial
44
+ change_category = nil
45
+ current_version = nil
46
+ @remainder = ""
47
+ log.each_line do |line|
48
+ case state
49
+ when :initial
50
+ next unless line.chomp == "\#\# [Unreleased]"
51
+ state = :unreleased
52
+ current_version = "Unreleased"
53
+ when :unreleased, :last_version
54
+ if /\A\#\#\# (?<category>\w+)/ =~ line
55
+ 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
59
+ current_version = version
60
+ state =
61
+ case state
62
+ when :unreleased
63
+ :last_version
64
+ else
65
+ :finished
66
+ end
67
+ elsif !line.match?(/\s*\n/)
68
+ raise "Unrecognized line: #{line.inspect}"
69
+ end
70
+ when :finished
71
+ @remainder << line
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -36,6 +36,13 @@ module ShopifyCLI
36
36
  end
37
37
  rescue OptionParser::InvalidOption => error
38
38
  arg = error.args.first
39
+ store_name = arg.match(/\A--(?<store_name>.*\.myshopify\.com)\z/)&.[](:store_name)
40
+ if store_name && !arg.match?(/\A--(store|shop)=/)
41
+ # Sometimes it may look like --invalidoption=https://storename.myshopify.com
42
+ store_name = store_name.sub(%r{\A(.*=)?(https?://)?}, "")
43
+ raise ShopifyCLI::Abort,
44
+ @ctx.message("core.errors.option_parser.invalid_option_store_equals", arg, store_name)
45
+ end
39
46
  raise ShopifyCLI::Abort, @ctx.message("core.errors.option_parser.invalid_option", arg)
40
47
  rescue OptionParser::MissingArgument => error
41
48
  arg = error.args.first
@@ -102,16 +109,10 @@ module ShopifyCLI
102
109
  def check_node_version
103
110
  return unless @compatible_node_range
104
111
 
105
- context = Context.new
106
- if context.which("node").nil?
107
- raise ShopifyCLI::Abort, context.message("core.errors.missing_node")
108
- end
109
-
110
112
  check_version(
111
113
  Environment.node_version,
112
114
  range: @compatible_node_range,
113
- runtime: "Node",
114
- context: context
115
+ runtime: "Node"
115
116
  )
116
117
  end
117
118
 
@@ -2,7 +2,6 @@ module ShopifyCLI
2
2
  module Commands
3
3
  class App
4
4
  class Deploy < ShopifyCLI::Command::AppSubCommand
5
- subcommand :Heroku, "heroku", "shopify_cli/commands/app/deploy/heroku"
6
5
  prerequisite_task :ensure_git_dependency
7
6
 
8
7
  recommend_default_ruby_range
@@ -103,7 +103,7 @@ module ShopifyCLI
103
103
  # any command run by the context.
104
104
  attr_accessor :env
105
105
 
106
- def initialize(root: Dir.pwd, env: ($original_env || ENV).clone) # :nodoc:
106
+ def initialize(root: Dir.pwd, env: ($original_env || ENV).to_h) # :nodoc:
107
107
  self.root = root
108
108
  self.env = env
109
109
  end
@@ -164,7 +164,7 @@ module ShopifyCLI
164
164
 
165
165
  # will return true while tests are running, either locally or on CI
166
166
  def testing?
167
- ci? || ENV["TEST"]
167
+ ci? || ENV["SHOPIFY_CLI_TEST"]
168
168
  end
169
169
 
170
170
  ##
@@ -5,7 +5,7 @@ module ShopifyCLI
5
5
  module EntryPoint
6
6
  class << self
7
7
  def call(args, ctx = Context.new)
8
- if ctx.development?
8
+ if ctx.development? && !ctx.testing?
9
9
  ctx.warn(
10
10
  ctx.message("core.warning.development_version", File.join(ShopifyCLI::ROOT, "bin", ShopifyCLI::TOOL_NAME))
11
11
  )
@@ -17,11 +17,7 @@ module ShopifyCLI
17
17
 
18
18
  def log(name, args, &block) # rubocop:disable Lint/UnusedMethodArgument
19
19
  command, command_name = Commands::Registry.lookup_command(name)
20
- final_command = [command_name]
21
- if command
22
- subcommand, subcommand_name = command.subcommand_registry.lookup_command(args.first)
23
- final_command << subcommand_name if subcommand
24
- end
20
+ full_command = self.full_command(command, args, resolved_command: [command_name])
25
21
 
26
22
  start_time = now_in_milliseconds
27
23
  err = nil
@@ -35,9 +31,21 @@ module ShopifyCLI
35
31
  # If there's an error, we don't prompt from here and we let the exception
36
32
  # reporter do that.
37
33
  if report?(prompt: err.nil?)
38
- send_event(start_time, final_command, args - final_command, err&.message)
34
+ send_event(start_time, full_command, args - full_command, err&.message)
35
+ end
36
+ end
37
+ end
38
+
39
+ def full_command(command, args, resolved_command:)
40
+ resolved_command = resolved_command.dup
41
+ if command
42
+ subcommand, subcommand_name = command.subcommand_registry.lookup_command(args.first)
43
+ resolved_command << subcommand_name if subcommand
44
+ if subcommand&.subcommand_registry
45
+ resolved_command = full_command(subcommand, args.drop(1), resolved_command: resolved_command)
39
46
  end
40
47
  end
48
+ resolved_command
41
49
  end
42
50
 
43
51
  private
@@ -12,23 +12,31 @@ module ShopifyCLI
12
12
  ]
13
13
 
14
14
  def self.ruby_version(context: Context.new)
15
- out, err, stat = context.capture3('ruby -e "puts RUBY_VERSION"')
16
- raise ShopifyCLI::Abort, err unless stat.success?
17
- out = out.gsub('"', "")
18
- ::Semantic::Version.new(out.chomp)
15
+ output, status = context.capture2e("ruby", "--version")
16
+ raise ShopifyCLI::Abort, context.message("core.errors.missing_ruby") unless status.success?
17
+ version = output.match(/ruby (\d+\.\d+\.\d+)/)[1]
18
+ ::Semantic::Version.new(version)
19
19
  end
20
20
 
21
21
  def self.node_version(context: Context.new)
22
- out, err, stat = context.capture3("node", "--version")
23
- raise ShopifyCLI::Abort, err unless stat.success?
24
- out = out.gsub("v", "")
25
- ::Semantic::Version.new(out.chomp)
22
+ output, status = context.capture2e("node", "--version")
23
+ raise ShopifyCLI::Abort, context.message("core.errors.missing_node") unless status.success?
24
+ version = output.match(/v(\d+\.\d+\.\d+)/)[1]
25
+ ::Semantic::Version.new(version)
26
26
  end
27
27
 
28
28
  def self.npm_version(context: Context.new)
29
- out, err, stat = context.capture3("npm", "--version")
30
- raise ShopifyCLI::Abort, err unless stat.success?
31
- ::Semantic::Version.new(out.chomp)
29
+ output, status = context.capture2e("npm", "--version")
30
+ raise ShopifyCLI::Abort, context.message("core.errors.missing_npm") unless status.success?
31
+ version = output.match(/(\d+\.\d+\.\d+)/)[1]
32
+ ::Semantic::Version.new(version)
33
+ end
34
+
35
+ def self.rails_version(context: Context.new)
36
+ output, status = context.capture2e("rails", "--version")
37
+ context.abort(context.message("core.app.create.rails.error.install_failure", "rails")) unless status.success?
38
+ version = output.match(/Rails (\d+\.\d+\.\d+)/)[1]
39
+ ::Semantic::Version.new(version)
32
40
  end
33
41
 
34
42
  def self.interactive=(interactive)
@@ -46,6 +46,8 @@ module ShopifyCLI
46
46
  Bugsnag.notify(error) do |event|
47
47
  event.add_metadata(:device, metadata)
48
48
  end
49
+ rescue
50
+ nil
49
51
  end
50
52
 
51
53
  def self.report?(context:)
@@ -15,9 +15,17 @@ module ShopifyCLI
15
15
  },
16
16
  core: {
17
17
  errors: {
18
- missing_node: "Node is required to continue. Install node here: https://nodejs.org/en/download.",
18
+ missing_node: "Node.js is required to continue. Install Node.js here: https://nodejs.org/en/download.",
19
+ missing_npm: "npm is required to continue. Install npm here: https://www.npmjs.com/get-npm.",
20
+ missing_ruby: "Ruby is required to continue. Install Ruby here: https://www.ruby-lang.org/en/downloads.",
19
21
  option_parser: {
20
22
  invalid_option: "The option {{command:%s}} is not supported.",
23
+ invalid_option_store_equals: <<~MESSAGE,
24
+ The option {{command:%s}} isn't recognized.
25
+
26
+ Try this:
27
+ {{command:--store=%s}}.
28
+ MESSAGE
21
29
  missing_argument: "The required argument {{command:%s}} is missing.",
22
30
  },
23
31
  },
@@ -95,8 +103,6 @@ module ShopifyCLI
95
103
  HELP
96
104
  error: {
97
105
  node_required: "node is required to create an app project. Download at https://nodejs.org/en/download.",
98
- node_version_failure: "Failed to get the current node version. Please make sure it is installed as " \
99
- "per the instructions at https://nodejs.org/en.",
100
106
  npm_required: "npm is required to create an app project. Download at https://www.npmjs.com/get-npm.",
101
107
  npm_version_failure: "Failed to get the current npm version. Please make sure it is installed as per " \
102
108
  "the instructions at https://www.npmjs.com/get-npm.",
@@ -131,8 +137,6 @@ module ShopifyCLI
131
137
  {{underline:https://getcomposer.org/download/}}
132
138
  COMPOSER
133
139
  npm_required: "npm is required to create an app project. Download at https://www.npmjs.com/get-npm.",
134
- npm_version_failure: "Failed to get the current npm version. Please make sure it is installed as per " \
135
- "the instructions at https://www.npmjs.com/get-npm.",
136
140
  app_setup: "Failed to set up the app",
137
141
  },
138
142
 
@@ -283,6 +287,8 @@ module ShopifyCLI
283
287
  localization: {
284
288
  error: {
285
289
  bundle_too_large: "Total size of all locale files must be less than %s.",
290
+ duplicate_locale_code: "Duplicate locale found: `%s`; locale codes"\
291
+ " should be unique and are case insensitive.",
286
292
  file_empty: "Locale file `%s` is empty.",
287
293
  file_too_large: "Locale file `%s` too large; size must be less than %s.",
288
294
  invalid_file_extension: "Invalid locale filename: `%s`; only .json files are allowed.",
@@ -81,7 +81,7 @@ module ShopifyCLI
81
81
  puts "Grabbing sha256 checksum from Rubygems.org"
82
82
  require "digest/sha2"
83
83
  require "open-uri"
84
- gem_checksum = open("https://rubygems.org/downloads/shopify-cli-#{ShopifyCLI::VERSION}.gem") do |io|
84
+ gem_checksum = URI.open("https://rubygems.org/downloads/shopify-cli-#{ShopifyCLI::VERSION}.gem") do |io|
85
85
  Digest::SHA256.new.hexdigest(io.read)
86
86
  end
87
87
 
@@ -0,0 +1,94 @@
1
+ require "shopify_cli/sed"
2
+ require "shopify_cli/changelog"
3
+ require "octokit"
4
+
5
+ module ShopifyCLI
6
+ class Release
7
+ def initialize(new_version, github_access_token)
8
+ @new_version = new_version
9
+ @changelog = ShopifyCLI::Changelog.new
10
+ @github = Octokit::Client.new(access_token: github_access_token)
11
+ end
12
+
13
+ def prepare!
14
+ ensure_updated_main
15
+ create_release_branch
16
+ update_changelog
17
+ update_versions_in_files
18
+ commit
19
+ pr = create_pr
20
+ system("open #{pr["html_url"]}")
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :new_version, :changelog, :github
26
+
27
+ def ensure_updated_main
28
+ current_branch = %x(git branch --show-current)
29
+ unless current_branch == "main"
30
+ raise "Must be on the main branch to package a release!"
31
+ end
32
+ unless system("git pull")
33
+ raise "git pull failed, cannot be sure there aren't new commits!"
34
+ end
35
+ end
36
+
37
+ def create_release_branch
38
+ puts "Checking out release branch"
39
+ unless system("git checkout -b #{release_branch_name}")
40
+ puts "Cannot check out release branch!"
41
+ end
42
+ end
43
+
44
+ def update_changelog
45
+ if release_notes.empty?
46
+ puts "No unreleased CHANGELOG updates found!"
47
+ else
48
+ puts "Updating CHANGELOG"
49
+ changelog.update_version!(new_version)
50
+ end
51
+ end
52
+
53
+ def update_versions_in_files
54
+ version_file = File.join(ShopifyCLI::ROOT, "lib/shopify_cli/version.rb")
55
+ puts "Updating version.rb"
56
+ ShopifyCLI::Sed.new.replace_inline(version_file, ShopifyCLI::VERSION, new_version)
57
+ gemfile_lock = File.join(ShopifyCLI::ROOT, "Gemfile.lock")
58
+ puts "Updating Gemfile.lock"
59
+ ShopifyCLI::Sed.new.replace_inline(
60
+ gemfile_lock,
61
+ "shopify-cli (#{ShopifyCLI::VERSION})",
62
+ "shopify-cli (#{new_version})",
63
+ )
64
+ end
65
+
66
+ def commit
67
+ puts "Committing"
68
+ unless system("git commit -am 'Packaging for release v#{new_version}'")
69
+ puts "Commit failed!"
70
+ end
71
+ unless system("git push -u origin #{release_branch_name}")
72
+ puts "Failed to push branch!"
73
+ end
74
+ end
75
+
76
+ def create_pr
77
+ github.create_pull_request(
78
+ "Shopify/shopify-cli",
79
+ "main",
80
+ release_branch_name,
81
+ "Packaging for release v#{new_version}",
82
+ release_notes
83
+ ).tap { |results| puts "Created PR ##{results["number"]}" }
84
+ end
85
+
86
+ def release_branch_name
87
+ @release_branch_name ||= "release_#{new_version.split(".").join("_")}"
88
+ end
89
+
90
+ def release_notes
91
+ @release_notes ||= changelog.release_notes("Unreleased")
92
+ end
93
+ end
94
+ end
@@ -181,6 +181,13 @@ module ShopifyCLI
181
181
  self
182
182
  end
183
183
 
184
+ ##
185
+ # returns the value this success represents
186
+ #
187
+ def unwrap!
188
+ value
189
+ end
190
+
184
191
  ##
185
192
  # returns the success value and ignores the fallback value that was either
186
193
  # provided as a method argument or by passing a block. However, the caller
@@ -339,6 +346,13 @@ module ShopifyCLI
339
346
  raise ArgumentError, "expected either a fallback value or a block" unless (args.length == 1) ^ block
340
347
  block ? block.call(@error) : args.pop
341
348
  end
349
+
350
+ ##
351
+ # raises the error this failure represents
352
+ #
353
+ def unwrap!
354
+ raise error
355
+ end
342
356
  end
343
357
 
344
358
  ##
@@ -0,0 +1,19 @@
1
+ module ShopifyCLI
2
+ class Sed
3
+ class SedError < StandardError; end
4
+
5
+ def replace_inline(filename, pattern, output)
6
+ command =
7
+ case CLI::Kit::System.os
8
+ when :mac
9
+ "sed -i ''"
10
+ when :linux
11
+ "sed -i"
12
+ else
13
+ raise "Unrecognized system!"
14
+ end
15
+ success = system("#{command} 's/#{pattern}/#{output}/' #{filename}")
16
+ raise SedError unless success
17
+ end
18
+ end
19
+ end