shopify-cli 2.9.0 → 2.11.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +117 -0
  3. data/.github/ISSUE_TEMPLATE/enhancement.yaml +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature.yaml +47 -0
  5. data/.github/ISSUE_TEMPLATE.md +18 -0
  6. data/CHANGELOG.md +38 -5
  7. data/Gemfile.lock +1 -1
  8. data/dev.yml +3 -0
  9. data/lib/project_types/extension/commands/build.rb +3 -0
  10. data/lib/project_types/extension/commands/check.rb +3 -0
  11. data/lib/project_types/extension/commands/create.rb +3 -0
  12. data/lib/project_types/extension/commands/push.rb +3 -0
  13. data/lib/project_types/extension/commands/serve.rb +3 -0
  14. data/lib/project_types/extension/models/specification_handlers/default.rb +1 -1
  15. data/lib/project_types/extension/tasks/convert_server_config.rb +3 -1
  16. data/lib/project_types/script/cli.rb +5 -0
  17. data/lib/project_types/script/commands/connect.rb +3 -1
  18. data/lib/project_types/script/commands/create.rb +2 -0
  19. data/lib/project_types/script/commands/push.rb +6 -0
  20. data/lib/project_types/script/config/extension_points.yml +12 -0
  21. data/lib/project_types/script/graphql/app_script_set.graphql +2 -0
  22. data/lib/project_types/script/graphql/module_upload_url_generate.graphql +5 -1
  23. data/lib/project_types/script/layers/application/build_script.rb +6 -3
  24. data/lib/project_types/script/layers/application/project_dependencies.rb +1 -1
  25. data/lib/project_types/script/layers/application/push_script.rb +38 -30
  26. data/lib/project_types/script/layers/domain/errors.rb +10 -3
  27. data/lib/project_types/script/layers/domain/extension_point.rb +2 -2
  28. data/lib/project_types/script/layers/domain/push_package.rb +0 -3
  29. data/lib/project_types/script/layers/domain/script_config.rb +6 -4
  30. data/lib/project_types/script/layers/domain/script_project.rb +1 -0
  31. data/lib/project_types/script/layers/infrastructure/errors.rb +47 -24
  32. data/lib/project_types/script/layers/infrastructure/languages/assemblyscript_task_runner.rb +2 -12
  33. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +1 -0
  34. data/lib/project_types/script/layers/infrastructure/languages/task_runner.rb +1 -0
  35. data/lib/project_types/script/layers/infrastructure/languages/typescript_task_runner.rb +2 -12
  36. data/lib/project_types/script/layers/infrastructure/languages/wasm_project_creator.rb +15 -0
  37. data/lib/project_types/script/layers/infrastructure/languages/wasm_task_runner.rb +36 -0
  38. data/lib/project_types/script/layers/infrastructure/metadata_repository.rb +18 -0
  39. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +7 -8
  40. data/lib/project_types/script/layers/infrastructure/script_project_repository.rb +45 -54
  41. data/lib/project_types/script/layers/infrastructure/script_service.rb +35 -12
  42. data/lib/project_types/script/layers/infrastructure/script_uploader.rb +22 -9
  43. data/lib/project_types/script/loaders/project.rb +2 -1
  44. data/lib/project_types/script/messages/messages.rb +94 -88
  45. data/lib/project_types/script/ui/error_handler.rb +75 -38
  46. data/lib/project_types/theme/commands/check.rb +3 -0
  47. data/lib/project_types/theme/commands/delete.rb +3 -0
  48. data/lib/project_types/theme/commands/init.rb +3 -0
  49. data/lib/project_types/theme/commands/language_server.rb +3 -0
  50. data/lib/project_types/theme/commands/package.rb +3 -0
  51. data/lib/project_types/theme/commands/publish.rb +3 -0
  52. data/lib/project_types/theme/commands/pull.rb +12 -1
  53. data/lib/project_types/theme/commands/push.rb +12 -1
  54. data/lib/project_types/theme/commands/serve.rb +3 -0
  55. data/lib/project_types/theme/messages/messages.rb +4 -0
  56. data/lib/shopify_cli/command/sub_command.rb +2 -0
  57. data/lib/shopify_cli/command.rb +66 -0
  58. data/lib/shopify_cli/commands/app/create/node.rb +3 -0
  59. data/lib/shopify_cli/commands/app/create/rails.rb +3 -0
  60. data/lib/shopify_cli/commands/app/create.rb +3 -0
  61. data/lib/shopify_cli/commands/app/deploy.rb +3 -0
  62. data/lib/shopify_cli/commands/app/serve.rb +3 -0
  63. data/lib/shopify_cli/commands/login.rb +4 -10
  64. data/lib/shopify_cli/constants.rb +18 -2
  65. data/lib/shopify_cli/core/executor.rb +4 -4
  66. data/lib/shopify_cli/environment.rb +61 -16
  67. data/lib/shopify_cli/exception_reporter.rb +9 -0
  68. data/lib/shopify_cli/github/issue_url_generator.rb +19 -8
  69. data/lib/shopify_cli/identity_auth/env_auth_token.rb +34 -0
  70. data/lib/shopify_cli/identity_auth.rb +36 -18
  71. data/lib/shopify_cli/messages/messages.rb +2 -2
  72. data/lib/shopify_cli/method_object.rb +21 -9
  73. data/lib/shopify_cli/partners_api.rb +7 -2
  74. data/lib/shopify_cli/result.rb +61 -59
  75. data/lib/shopify_cli/services/app/create/rails_service.rb +37 -13
  76. data/lib/shopify_cli/task.rb +5 -3
  77. data/lib/shopify_cli/theme/file.rb +2 -2
  78. data/lib/shopify_cli/theme/filter/path_matcher.rb +38 -0
  79. data/lib/shopify_cli/theme/ignore_filter.rb +14 -18
  80. data/lib/shopify_cli/theme/include_filter.rb +65 -0
  81. data/lib/shopify_cli/theme/syncer.rb +17 -2
  82. data/lib/shopify_cli/utilities.rb +7 -0
  83. data/lib/shopify_cli/version.rb +1 -1
  84. data/lib/shopify_cli.rb +3 -1
  85. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +11 -6
  86. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +5 -1
  87. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +6 -4
  88. data/vendor/deps/ruby2_keywords/LICENSE +22 -0
  89. data/vendor/deps/ruby2_keywords/README.md +67 -0
  90. data/vendor/deps/ruby2_keywords/Rakefile +54 -0
  91. data/vendor/deps/ruby2_keywords/lib/ruby2_keywords.rb +57 -0
  92. data/vendor/deps/ruby2_keywords/ruby2_keywords.gemspec +18 -0
  93. data/vendor/deps/ruby2_keywords/test/test_keyword.rb +41 -0
  94. data/vendor/lib/semantic/version.rb +0 -1
  95. metadata +18 -3
  96. data/lib/project_types/rails/commands/create.rb +0 -210
@@ -2,11 +2,15 @@
2
2
  require "shopify_cli/theme/theme"
3
3
  require "shopify_cli/theme/development_theme"
4
4
  require "shopify_cli/theme/ignore_filter"
5
+ require "shopify_cli/theme/include_filter"
5
6
  require "shopify_cli/theme/syncer"
6
7
 
7
8
  module Theme
8
9
  class Command
9
10
  class Push < ShopifyCLI::Command::SubCommand
11
+ recommend_default_node_range
12
+ recommend_default_ruby_range
13
+
10
14
  options do |parser, flags|
11
15
  parser.on("-n", "--nodelete") { flags[:nodelete] = true }
12
16
  parser.on("-i", "--themeid=ID") { |theme_id| flags[:theme_id] = theme_id }
@@ -17,6 +21,10 @@ module Theme
17
21
  parser.on("-j", "--json") { flags[:json] = true }
18
22
  parser.on("-a", "--allow-live") { flags[:allow_live] = true }
19
23
  parser.on("-p", "--publish") { flags[:publish] = true }
24
+ parser.on("-o", "--only=PATTERN") do |pattern|
25
+ flags[:includes] ||= []
26
+ flags[:includes] << pattern
27
+ end
20
28
  parser.on("-x", "--ignore=PATTERN") do |pattern|
21
29
  flags[:ignores] ||= []
22
30
  flags[:ignores] << pattern
@@ -35,10 +43,13 @@ module Theme
35
43
  return unless CLI::UI::Prompt.confirm(question)
36
44
  end
37
45
 
46
+ include_filter = ShopifyCLI::Theme::IncludeFilter.new(options.flags[:includes])
38
47
  ignore_filter = ShopifyCLI::Theme::IgnoreFilter.from_path(root)
39
48
  ignore_filter.add_patterns(options.flags[:ignores]) if options.flags[:ignores]
40
49
 
41
- syncer = ShopifyCLI::Theme::Syncer.new(@ctx, theme: theme, ignore_filter: ignore_filter)
50
+ syncer = ShopifyCLI::Theme::Syncer.new(@ctx, theme: theme,
51
+ include_filter: include_filter,
52
+ ignore_filter: ignore_filter)
42
53
  begin
43
54
  syncer.start_threads
44
55
  if options.flags[:json]
@@ -4,6 +4,9 @@ require "shopify_cli/theme/dev_server"
4
4
  module Theme
5
5
  class Command
6
6
  class Serve < ShopifyCLI::Command::SubCommand
7
+ recommend_default_node_range
8
+ recommend_default_ruby_range
9
+
7
10
  DEFAULT_HTTP_HOST = "127.0.0.1"
8
11
 
9
12
  options do |parser, flags|
@@ -65,6 +65,8 @@ module Theme
65
65
  {{command:-j, --json}} Output JSON instead of a UI.
66
66
  {{command:-a, --allow-live}} Allow push to a live theme.
67
67
  {{command:-p, --publish}} Publish as the live theme after uploading.
68
+ {{command:-o, --only}} Upload only the specified files.
69
+ {{command:-x, --ignore}} Skip uploading the specified files.
68
70
 
69
71
  Run without options to select theme from a list.
70
72
  HELP
@@ -200,6 +202,8 @@ module Theme
200
202
  {{command:-l, --live}} Pull theme files from your remote live theme.
201
203
  {{command:-d, --development}} Pull theme files from your remote development theme.
202
204
  {{command:-n, --nodelete}} Runs the pull command without deleting local files.
205
+ {{command:-o, --only}} Download only the specified files.
206
+ {{command:-x, --ignore}} Skip downloading the specified files.
203
207
 
204
208
  Run without options to select theme from a list.
205
209
  HELP
@@ -9,6 +9,8 @@ module ShopifyCLI
9
9
  cmd = new(@ctx)
10
10
  args = cmd.options.parse(@_options, args || [])
11
11
  return call_help(parent_command, command_name) if cmd.options.help
12
+ check_ruby_version
13
+ check_node_version
12
14
  run_prerequisites
13
15
 
14
16
  cmd.call(args, command_name)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "shopify_cli"
3
+ require "semantic/semantic"
3
4
 
4
5
  module ShopifyCLI
5
6
  class Command < CLI::Kit::BaseCommand
@@ -7,6 +8,8 @@ module ShopifyCLI
7
8
  autoload :AppSubCommand, "shopify_cli/command/app_sub_command"
8
9
  autoload :ProjectCommand, "shopify_cli/command/project_command"
9
10
 
11
+ VersionRange = Struct.new(:from, :to, keyword_init: true)
12
+
10
13
  extend Feature::Set
11
14
 
12
15
  attr_writer :ctx
@@ -26,6 +29,8 @@ module ShopifyCLI
26
29
  cmd = new(@ctx)
27
30
  cmd.options.parse(@_options, args)
28
31
  return call_help(command_name) if cmd.options.help
32
+ check_ruby_version
33
+ check_node_version
29
34
  run_prerequisites
30
35
  cmd.call(args, command_name)
31
36
  end
@@ -58,6 +63,67 @@ module ShopifyCLI
58
63
  )
59
64
  end
60
65
 
66
+ def recommend_ruby(from:, to:)
67
+ @compatible_ruby_range = VersionRange.new(
68
+ from: Semantic::Version.new(from),
69
+ to: Semantic::Version.new(to)
70
+ )
71
+ end
72
+
73
+ def recommend_default_ruby_range
74
+ recommend_ruby(
75
+ from: Constants::SupportedVersions::Ruby::FROM,
76
+ to: Constants::SupportedVersions::Ruby::TO
77
+ )
78
+ end
79
+
80
+ def check_ruby_version
81
+ check_version(
82
+ Environment.ruby_version,
83
+ range: @compatible_ruby_range,
84
+ runtime: "Ruby"
85
+ )
86
+ end
87
+
88
+ def recommend_node(from:, to:)
89
+ @compatible_node_range = VersionRange.new(
90
+ from: Semantic::Version.new(from),
91
+ to: Semantic::Version.new(to)
92
+ )
93
+ end
94
+
95
+ def recommend_default_node_range
96
+ recommend_node(
97
+ from: Constants::SupportedVersions::Node::FROM,
98
+ to: Constants::SupportedVersions::Node::TO
99
+ )
100
+ end
101
+
102
+ def check_node_version
103
+ check_version(
104
+ Environment.node_version,
105
+ range: @compatible_node_range,
106
+ runtime: "Node"
107
+ )
108
+ end
109
+
110
+ def check_version(version, range:, runtime:)
111
+ return if Environment.test?
112
+ return if range.nil?
113
+
114
+ version_without_pre_nor_build = Utilities.version_dropping_pre_and_build(version)
115
+ is_higher_than_bottom = version_without_pre_nor_build >= Utilities.version_dropping_pre_and_build(range.from)
116
+ is_lower_than_top = version_without_pre_nor_build < Utilities.version_dropping_pre_and_build(range.to)
117
+ return if is_higher_than_bottom && is_lower_than_top
118
+
119
+ Context.new.warn("Your environment #{runtime} version, #{version},"\
120
+ " is outside of the range supported by the CLI,"\
121
+ " #{range.from}..<#{range.to},"\
122
+ " and might cause incompatibility issues.")
123
+ rescue StandardError => error
124
+ ExceptionReporter.report_error_silently(error)
125
+ end
126
+
61
127
  def prerequisite_task(*tasks_without_args, **tasks_with_args)
62
128
  @prerequisite_tasks ||= []
63
129
  @prerequisite_tasks += tasks_without_args.map { |t| PrerequisiteTask.new(t) }
@@ -5,6 +5,9 @@ module ShopifyCLI
5
5
  class Node < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
7
 
8
+ recommend_default_node_range
9
+ recommend_default_ruby_range
10
+
8
11
  options do |parser, flags|
9
12
  parser.on("--name=NAME") { |t| flags[:name] = t }
10
13
  parser.on("--organization-id=ID") { |id| flags[:organization_id] = id }
@@ -5,6 +5,9 @@ module ShopifyCLI
5
5
  class Rails < ShopifyCLI::Command::AppSubCommand
6
6
  prerequisite_task :ensure_authenticated
7
7
 
8
+ recommend_default_node_range
9
+ recommend_default_ruby_range
10
+
8
11
  options do |parser, flags|
9
12
  parser.on("--name=NAME") { |t| flags[:name] = t }
10
13
  parser.on("--organization-id=ID") { |id| flags[:organization_id] = id }
@@ -6,6 +6,9 @@ module ShopifyCLI
6
6
  subcommand :PHP, "php", "shopify_cli/commands/app/create/php"
7
7
  subcommand :Node, "node", "shopify_cli/commands/app/create/node"
8
8
 
9
+ recommend_default_node_range
10
+ recommend_default_ruby_range
11
+
9
12
  def call(_args, _command_name)
10
13
  @ctx.puts(self.class.help)
11
14
  end
@@ -4,6 +4,9 @@ module ShopifyCLI
4
4
  class Deploy < ShopifyCLI::Command::AppSubCommand
5
5
  subcommand :Heroku, "heroku", "shopify_cli/commands/app/deploy/heroku"
6
6
 
7
+ recommend_default_node_range
8
+ recommend_default_ruby_range
9
+
7
10
  def call(args, _name)
8
11
  platform = args.shift
9
12
  case platform
@@ -6,6 +6,9 @@ module ShopifyCLI
6
6
 
7
7
  prerequisite_task :ensure_env, :ensure_dev_store
8
8
 
9
+ recommend_default_ruby_range
10
+ recommend_default_node_range
11
+
9
12
  options do |parser, flags|
10
13
  parser.on("--host=HOST") do |h|
11
14
  flags[:host] = h.gsub('"', "")
@@ -15,6 +15,8 @@ module ShopifyCLI
15
15
 
16
16
  def call(*)
17
17
  shop = (options.flags[:shop] || @ctx.getenv("SHOPIFY_SHOP" || nil))
18
+ ShopifyCLI::DB.set(shop: self.class.validate_shop(shop, context: @ctx)) unless shop.nil?
19
+
18
20
  if shop.nil? && Shopifolk.check
19
21
  Shopifolk.reset
20
22
  @ctx.puts(@ctx.message("core.tasks.select_org_and_shop.identified_as_shopify"))
@@ -31,25 +33,17 @@ module ShopifyCLI
31
33
  IdentityAuth.new(ctx: @ctx).authenticate
32
34
  org = select_organization
33
35
  ShopifyCLI::DB.set(organization_id: org["id"].to_i) unless org.nil?
34
-
36
+ Whoami.call([], "whoami")
35
37
  end
36
- # validate that shop belongs to organization
37
- ShopifyCLI::DB.set(shop: self.class.validate_shop(shop: shop, org: org, context: @ctx)) unless shop.nil?
38
- Whoami.call([], "whoami")
39
38
  end
40
39
 
41
40
  def self.help
42
41
  ShopifyCLI::Context.message("core.login.help", ShopifyCLI::TOOL_NAME)
43
42
  end
44
43
 
45
- def self.validate_shop(shop:, org:, context:)
44
+ def self.validate_shop(shop, context:)
46
45
  permanent_domain = shop_to_permanent_domain(shop)
47
46
  context.abort(context.message("core.login.invalid_shop", shop)) unless permanent_domain
48
- if org
49
- stores_owned = org["stores"]
50
- is_verified = stores_owned.any? { |store| store["shopDomain"] == permanent_domain }
51
- context.abort(context.message("core.login.invalid_shop", shop)) unless is_verified
52
- end
53
47
  permanent_domain
54
48
  end
55
49
 
@@ -36,12 +36,16 @@ module ShopifyCLI
36
36
  # the partners dashboard and identity.
37
37
  LOCAL_PARTNERS = "SHOPIFY_APP_CLI_LOCAL_PARTNERS"
38
38
 
39
- # When true the CLI points to a spin instance of spin
40
- SPIN_PARTNERS = "SHOPIFY_APP_CLI_SPIN_PARTNERS"
39
+ # When true the CLI points to spin instances of services
40
+ SPIN = "SPIN"
41
+ INFER_SPIN = "INFER_SPIN"
41
42
  SPIN_WORKSPACE = "SPIN_WORKSPACE"
42
43
  SPIN_NAMESPACE = "SPIN_NAMESPACE"
43
44
  SPIN_HOST = "SPIN_HOST"
44
45
 
46
+ # Deprecated, equivalent to using SPIN=1
47
+ SPIN_PARTNERS = "SHOPIFY_APP_CLI_SPIN_PARTNERS"
48
+
45
49
  # Environments
46
50
  TEST = "SHOPIFY_CLI_TEST"
47
51
  ACCEPTANCE_TEST = "SHOPIFY_CLI_ACCEPTANCE_TEST"
@@ -54,6 +58,18 @@ module ShopifyCLI
54
58
  MONORAIL_REAL_EVENTS = "MONORAIL_REAL_EVENTS"
55
59
  end
56
60
 
61
+ module SupportedVersions
62
+ module Ruby
63
+ FROM = "2.6.6"
64
+ TO = "3.1.0"
65
+ end
66
+
67
+ module Node
68
+ FROM = "12.0.0"
69
+ TO = "17.0.0"
70
+ end
71
+ end
72
+
57
73
  module Identity
58
74
  CLIENT_ID_DEV = "e5380e02-312a-7408-5718-e07017e9cf52"
59
75
  CLIENT_ID = "fbdb2649-e327-4907-8f67-908d24cfd7e3"
@@ -3,10 +3,10 @@ require "shopify_cli"
3
3
  module ShopifyCLI
4
4
  module Core
5
5
  class Executor < CLI::Kit::Executor
6
- def initialize(ctx, task_registry, *args, **kwargs)
7
- @ctx = ctx || ShopifyCLI::Context.new
8
- @task_registry = task_registry || ShopifyCLI::Tasks::TaskRegistry.new
9
- super(*args, **kwargs)
6
+ ruby2_keywords def initialize(ctx, task_registry, *args)
7
+ @ctx = ctx || ShopifyCli::Context.new
8
+ @task_registry = task_registry || ShopifyCli::Tasks::TaskRegistry.new
9
+ super(*args)
10
10
  end
11
11
 
12
12
  def call(command, command_name, args)
@@ -1,9 +1,25 @@
1
+ require "semantic/semantic"
2
+
1
3
  module ShopifyCLI
2
4
  # The environment module provides an interface to get information from
3
5
  # the environment in which the CLI runs
4
6
  module Environment
5
7
  TRUTHY_ENV_VARIABLE_VALUES = ["1", "true", "TRUE", "yes", "YES"]
6
8
 
9
+ def self.ruby_version(context: Context.new)
10
+ out, err, stat = context.capture3('ruby -e "puts RUBY_VERSION"')
11
+ raise ShopifyCLI::Abort, err unless stat.success?
12
+ out = out.gsub('"', "")
13
+ ::Semantic::Version.new(out.chomp)
14
+ end
15
+
16
+ def self.node_version(context: Context.new)
17
+ out, err, stat = context.capture3("node", "--version")
18
+ raise ShopifyCLI::Abort, err unless stat.success?
19
+ out = out.gsub("v", "")
20
+ ::Semantic::Version.new(out.chomp)
21
+ end
22
+
7
23
  def self.interactive=(interactive)
8
24
  @interactive = interactive
9
25
  end
@@ -61,17 +77,10 @@ module ShopifyCLI
61
77
  )
62
78
  end
63
79
 
64
- def self.use_spin_partners_instance?(env_variables: ENV)
65
- env_variable_truthy?(
66
- Constants::EnvironmentVariables::SPIN_PARTNERS,
67
- env_variables: env_variables
68
- )
69
- end
70
-
71
80
  def self.partners_domain(env_variables: ENV)
72
81
  if use_local_partners_instance?(env_variables: env_variables)
73
82
  "partners.myshopify.io"
74
- elsif use_spin_partners_instance?(env_variables: env_variables)
83
+ elsif use_spin?(env_variables: env_variables)
75
84
  "partners.#{spin_url(env_variables: env_variables)}"
76
85
  else
77
86
  "partners.shopify.com"
@@ -79,15 +88,41 @@ module ShopifyCLI
79
88
  end
80
89
 
81
90
  def self.use_spin?(env_variables: ENV)
82
- !env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE].nil? &&
83
- !env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE].nil?
91
+ env_variable_truthy?(
92
+ Constants::EnvironmentVariables::SPIN,
93
+ env_variables: env_variables
94
+ ) || env_variable_truthy?(
95
+ Constants::EnvironmentVariables::SPIN_PARTNERS,
96
+ env_variables: env_variables
97
+ )
98
+ end
99
+
100
+ def self.infer_spin?(env_variables: ENV)
101
+ env_variable_truthy?(
102
+ Constants::EnvironmentVariables::INFER_SPIN,
103
+ env_variables: env_variables
104
+ )
84
105
  end
85
106
 
86
107
  def self.spin_url(env_variables: ENV)
87
- spin_workspace = spin_workspace(env_variables: env_variables)
88
- spin_namespace = spin_namespace(env_variables: env_variables)
89
- spin_host = spin_host(env_variables: env_variables)
90
- "#{spin_workspace}.#{spin_namespace}.#{spin_host}"
108
+ if infer_spin?(env_variables: env_variables)
109
+ # TODO: Remove version check and delete spin-legacy branch
110
+ # once spin2 becomes the installed "spin" binary by default
111
+ spin_version = %x(spin version 2> /dev/null).strip
112
+ if spin_version.start_with?("spin-")
113
+ # spin2
114
+ raise ShopifyCLI:: Abort, "SPIN_INSTANCE must be specified" unless ENV.key?("SPIN_INSTANCE")
115
+ %x(spin show -o fqdn 2> /dev/null).strip
116
+ else
117
+ # spin-legacy
118
+ %x(spin info fqdn 2> /dev/null).strip
119
+ end
120
+ else
121
+ spin_workspace = spin_workspace(env_variables: env_variables)
122
+ spin_namespace = spin_namespace(env_variables: env_variables)
123
+ spin_host = spin_host(env_variables: env_variables)
124
+ "#{spin_workspace}.#{spin_namespace}.#{spin_host}"
125
+ end
91
126
  end
92
127
 
93
128
  def self.send_monorail_events?(env_variables: ENV)
@@ -106,11 +141,21 @@ module ShopifyCLI
106
141
  end
107
142
 
108
143
  def self.spin_workspace(env_variables: ENV)
109
- env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE]
144
+ env_value = env_variables[Constants::EnvironmentVariables::SPIN_WORKSPACE]
145
+ return env_value unless env_value.nil?
146
+
147
+ if env_value.nil?
148
+ raise "No value set for #{Constants::EnvironmentVariables::SPIN_WORKSPACE}"
149
+ end
110
150
  end
111
151
 
112
152
  def self.spin_namespace(env_variables: ENV)
113
- env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE]
153
+ env_value = env_variables[Constants::EnvironmentVariables::SPIN_NAMESPACE]
154
+ return env_value unless env_value.nil?
155
+
156
+ if env_value.nil?
157
+ raise "No value set for #{Constants::EnvironmentVariables::SPIN_NAMESPACE}"
158
+ end
114
159
  end
115
160
 
116
161
  def self.spin_host(env_variables: ENV)
@@ -1,5 +1,10 @@
1
1
  module ShopifyCLI
2
2
  module ExceptionReporter
3
+ def self.report_error_silently(error)
4
+ return unless ReportingConfigurationController.reporting_enabled?
5
+ report_to_bugsnag(error: error)
6
+ end
7
+
3
8
  def self.report(error, _logs = nil, _api_key = nil, custom_metadata = {})
4
9
  context = ShopifyCLI::Context.new
5
10
  unless ShopifyCLI::Environment.development?
@@ -19,6 +24,10 @@ module ShopifyCLI
19
24
  return unless reportable_error?(error)
20
25
 
21
26
  return unless report?(context: context)
27
+ report_to_bugsnag(error: error, custom_metadata: custom_metadata)
28
+ end
29
+
30
+ def self.report_to_bugsnag(error:, custom_metadata: {})
22
31
  ENV["BUGSNAG_DISABLE_AUTOCONFIGURE"] = "1"
23
32
  require "bugsnag"
24
33
 
@@ -2,17 +2,28 @@ module ShopifyCLI
2
2
  module GitHub
3
3
  module IssueURLGenerator
4
4
  def self.error_url(error)
5
- title = "#{error.class}: #{error.message}"
5
+ title = "[Bug]: #{error.class}: #{error.message}"
6
6
  labels = "type:bug"
7
- content = File.read(File.join(ShopifyCLI::ROOT, ".github/ISSUE_TEMPLATE.md"))
8
7
 
9
8
  # take at most 5 lines from backtrace
10
- stacktrace = error.backtrace.length < 5 ? error.backtrace : error.backtrace[0..4]
11
- body = stacktrace.join("\n").to_s
12
- output = content.gsub(/<!--Stacktrace(.|\n)*-->/, body)
13
- query = URI.encode_www_form({ title: title, body: output, labels: labels })
14
- url = "#{ShopifyCLI::Constants::Links::NEW_ISSUE}?#{query}"
15
- url
9
+ stacktrace_text =
10
+ if error.backtrace # Sometimes errors seem to appear without backtrace, see https://github.com/Shopify/shopify-cli/issues/1972#issuecomment-1028013630
11
+ stacktrace = error.backtrace.length < 5 ? error.backtrace : error.backtrace[0..4]
12
+ stacktrace.join("\n").to_s
13
+ else
14
+ ""
15
+ end
16
+ query = URI.encode_www_form({
17
+ title: title,
18
+ labels: labels,
19
+ template: "bug_report.yaml",
20
+ stack_trace: stacktrace_text,
21
+ os: RUBY_PLATFORM,
22
+ cli_version: ShopifyCLI::VERSION,
23
+ ruby_version: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}",
24
+ shell: ENV["SHELL"],
25
+ })
26
+ "#{ShopifyCLI::Constants::Links::NEW_ISSUE}?#{query}"
16
27
  end
17
28
  end
18
29
  end
@@ -0,0 +1,34 @@
1
+ module ShopifyCLI
2
+ class IdentityAuth
3
+ class EnvAuthToken
4
+ Token = Struct.new(:token, :expires_at, keyword_init: true)
5
+
6
+ class << self
7
+ attr_accessor :exchanged_partners_token
8
+
9
+ def partners_token_present?
10
+ Environment.auth_token
11
+ end
12
+
13
+ def fetch_exchanged_partners_token
14
+ current_time = Time.now.to_i
15
+
16
+ # If we have an in-memory token that hasn't expired yet, we reuse it.
17
+ if exchanged_partners_token && current_time < exchanged_partners_token.expires_at.to_i
18
+ return exchanged_partners_token.token
19
+ end
20
+
21
+ new_exchanged_token = yield(Environment.auth_token)
22
+ token = new_exchanged_token["access_token"]
23
+ expires_in = new_exchanged_token["expires_in"].to_i
24
+ expires_at = Time.at(current_time + expires_in)
25
+
26
+ token = Token.new(token: token, expires_at: expires_at)
27
+
28
+ self.exchanged_partners_token = token
29
+ token.token
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -13,6 +13,7 @@ module ShopifyCLI
13
13
  include SmartProperties
14
14
 
15
15
  autoload :Servlet, "shopify_cli/identity_auth/servlet"
16
+ autoload :EnvAuthToken, "shopify_cli/identity_auth/env_auth_token"
16
17
 
17
18
  class Error < StandardError; end
18
19
  class Timeout < StandardError; end
@@ -68,9 +69,12 @@ module ShopifyCLI
68
69
  request_exchange_tokens
69
70
  end
70
71
 
71
- def self.fetch_or_auth_partners_token(ctx:)
72
- env_var_auth_token = Environment.auth_token
73
- return env_var_auth_token if env_var_auth_token
72
+ def fetch_or_auth_partners_token
73
+ if EnvAuthToken.partners_token_present?
74
+ return EnvAuthToken.fetch_exchanged_partners_token do |env_token|
75
+ exchange_partners_auth_token(env_token)
76
+ end
77
+ end
74
78
 
75
79
  ShopifyCLI::DB.get(:partners_exchange_token) do
76
80
  IdentityAuth.new(ctx: ctx).authenticate
@@ -78,6 +82,15 @@ module ShopifyCLI
78
82
  end
79
83
  end
80
84
 
85
+ def exchange_partners_auth_token(subject_token)
86
+ application = "partners"
87
+ request_exchange_token(
88
+ audience: client_id_for_application(application),
89
+ scopes: APPLICATION_SCOPES[application],
90
+ subject_token: subject_token,
91
+ )
92
+ end
93
+
81
94
  def self.environment_auth_token?
82
95
  !!Environment.auth_token
83
96
  end
@@ -195,30 +208,35 @@ module ShopifyCLI
195
208
 
196
209
  def request_exchange_tokens
197
210
  APPLICATION_SCOPES.each do |key, scopes|
198
- request_exchange_token(key, client_id_for_application(key), scopes)
211
+ request_and_save_exchange_token(key, client_id_for_application(key), scopes)
199
212
  end
200
213
  end
201
214
 
202
- def request_exchange_token(name, audience, additional_scopes)
215
+ def request_and_save_exchange_token(name, audience, additional_scopes)
203
216
  return if name == "shopify" && !store.exists?(:shop)
217
+ access_token = request_exchange_token(
218
+ audience: audience,
219
+ scopes: scopes(additional_scopes),
220
+ subject_token: store.get(:identity_access_token),
221
+ destination: name == "shopify" ? "https://#{store.get(:shop)}/admin" : nil
222
+ )["access_token"]
223
+ store.set("#{name}_exchange_token".to_sym => access_token)
224
+ ctx.debug("#{name}_exchange_token: " + access_token)
225
+ end
204
226
 
227
+ def request_exchange_token(audience:, scopes:, subject_token:, destination: nil)
205
228
  params = {
206
229
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
207
230
  requested_token_type: "urn:ietf:params:oauth:token-type:access_token",
208
231
  subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
209
232
  client_id: client_id,
210
233
  audience: audience,
211
- scope: scopes(additional_scopes),
212
- subject_token: store.get(:identity_access_token),
213
- }.tap do |result|
214
- if name == "shopify"
215
- result[:destination] = "https://#{store.get(:shop)}/admin"
216
- end
217
- end
234
+ scope: scopes,
235
+ subject_token: subject_token,
236
+ destination: destination,
237
+ }.compact
218
238
  # ctx.debug(params)
219
- resp = post_token_request(params)
220
- store.set("#{name}_exchange_token".to_sym => resp["access_token"])
221
- ctx.debug("#{name}_exchange_token: " + resp["access_token"])
239
+ post_token_request(params)
222
240
  end
223
241
 
224
242
  def post_token_request(params)
@@ -256,7 +274,7 @@ module ShopifyCLI
256
274
  def auth_url
257
275
  if Environment.use_local_partners_instance?
258
276
  "https://identity.myshopify.io/oauth"
259
- elsif Environment.use_spin_partners_instance?
277
+ elsif Environment.use_spin?
260
278
  "https://identity.#{Environment.spin_url}/oauth"
261
279
  else
262
280
  "https://accounts.shopify.com/oauth"
@@ -264,7 +282,7 @@ module ShopifyCLI
264
282
  end
265
283
 
266
284
  def client_id_for_application(application_name)
267
- client_ids = if Environment.use_local_partners_instance? || Environment.use_spin_partners_instance?
285
+ client_ids = if Environment.use_local_partners_instance? || Environment.use_spin?
268
286
  DEV_APPLICATION_CLIENT_IDS
269
287
  else
270
288
  APPLICATION_CLIENT_IDS
@@ -280,7 +298,7 @@ module ShopifyCLI
280
298
  end
281
299
 
282
300
  def client_id
283
- if Environment.use_local_partners_instance? || Environment.use_spin_partners_instance?
301
+ if Environment.use_local_partners_instance? || Environment.use_spin?
284
302
  Constants::Identity::CLIENT_ID_DEV
285
303
  else
286
304
  # In the future we might want to use Identity's dynamic
@@ -53,7 +53,7 @@ module ShopifyCLI
53
53
  {{command:--organization-id=ID}} Partner organization ID. Must be an existing organization.
54
54
  {{command:--store-domain=MYSHOPIFYDOMAIN }} Development store URL. Must be an existing development store.
55
55
  {{command:--db=DB}} Database type. Must be one of: mysql, postgresql, sqlite3, oracle, frontbase, ibm_db, sqlserver, jdbcmysql, jdbcsqlite3, jdbcpostgresql, jdbc.
56
- {{command:--rails-opts=RAILSOPTS}} Additional options. Must be string containing one or more valid Rails options, separated by spaces.
56
+ {{command:--rails-opts=RAILSOPTS}} Additional options. Must be a string containing one or more valid Rails options, separated by spaces.
57
57
  HELP
58
58
 
59
59
  error: {
@@ -415,7 +415,7 @@ module ShopifyCLI
415
415
  Usage: {{command:%s login [--store=STORE]}}
416
416
  HELP
417
417
  invalid_shop: <<~MESSAGE,
418
- Invalid store provided (%s). Please make sure that the store belongs to your partner organization, and provide the store in the following format: my-store.myshopify.com
418
+ Invalid store provided (%s). Please provide the store in the following format: my-store.myshopify.com
419
419
  MESSAGE
420
420
  shop_prompt: <<~PROMPT,
421
421
  What store are you connecting to? (e.g. my-store.myshopify.com; do {{bold:NOT}} include protocol part, e.g., https://)