shopify-cli 2.13.0 → 2.15.1

Sign up to get free protection for your applications and to get access to all the features.
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