mysigner 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4297e99c0337f28d14aea62d52242a57db707a59a23ad2d2890271c9759c3ac1
4
- data.tar.gz: 56078eddbf0b0a103a27378abcaa8de232774e1185c8b410a19ec9c3160b08ab
3
+ metadata.gz: f363c20471bed89a90cb71ac4b6762cbeb5f05e3a7a015dd1e6b0f9a31186561
4
+ data.tar.gz: ea0bebacee050e567361bdd3d0803e74520fefe563ccfbc1bd83f857adc5cecf
5
5
  SHA512:
6
- metadata.gz: 6ff3e8130cba10fa931e4f89d9de87fb4acafa37203d572f709021534b0a2dce6ae7cdcc26608a641e21835e6808671702ae73b6635f84e5787f302b5b8e92df
7
- data.tar.gz: e7ffb3d8fe8b00ca4d3fe179a97d95a37982f67789f17048132d2be18c6eb23ef574e1a20783d72173002c9bd9ee063dec3206219af13dea6769f787f293d5cb
6
+ metadata.gz: c2bdc172b95d45eec70359764baa3b6783ded9ec59798144521ea57c19e09d616d306ef18501428f568dc44204b950c9cde17b34e436f95c9d9f1d821ed3794a
7
+ data.tar.gz: 87ee8d47ef06e8b8fb2dbb96676a75e233b183702b3012b9a4864445ff7653100c2166504384c18a7220c4f4ff18f041a94dc2c7f417850eadcfab076f1ca2ce
data/.gitignore CHANGED
@@ -11,3 +11,7 @@
11
11
 
12
12
  # rspec failure tracking
13
13
  .rspec_status
14
+
15
+ # macOS
16
+ .DS_Store
17
+ **/.DS_Store
data/.rubocop_todo.yml CHANGED
@@ -1,12 +1,12 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config --auto-gen-only-exclude`
3
- # on 2026-03-31 15:30:27 UTC using RuboCop version 1.86.0.
3
+ # on 2026-04-24 18:30:19 UTC using RuboCop version 1.86.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 5
9
+ # Offense count: 7
10
10
  # This cop supports safe autocorrection (--autocorrect).
11
11
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
12
12
  # URISchemes: http, https
@@ -18,7 +18,18 @@ Layout/LineLength:
18
18
  - 'mysigner.gemspec'
19
19
  - 'spec/cli/ship_appstore_spec.rb'
20
20
 
21
- # Offense count: 44
21
+ # Offense count: 2
22
+ # This cop supports unsafe autocorrection (--autocorrect-all).
23
+ Lint/NonAtomicFileOperation:
24
+ Exclude:
25
+ - 'lib/mysigner/cli/resource_commands.rb'
26
+
27
+ # Offense count: 12
28
+ Lint/UnreachableCode:
29
+ Exclude:
30
+ - 'lib/mysigner/cli/resource_commands.rb'
31
+
32
+ # Offense count: 45
22
33
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
23
34
  Metrics/AbcSize:
24
35
  Exclude:
@@ -30,6 +41,7 @@ Metrics/AbcSize:
30
41
  - 'lib/mysigner/cli/validate_commands.rb'
31
42
  - 'lib/mysigner/signing/validator.rb'
32
43
  - 'lib/mysigner/signing/wizard.rb'
44
+ - 'lib/mysigner/upload/app_store_automation.rb'
33
45
  - 'lib/mysigner/upload/app_store_submission.rb'
34
46
 
35
47
  # Offense count: 11
@@ -37,8 +49,6 @@ Metrics/AbcSize:
37
49
  # AllowedMethods: refine
38
50
  Metrics/BlockLength:
39
51
  Exclude:
40
- - 'spec/**/*'
41
- - '*.gemspec'
42
52
  - 'lib/mysigner/cli/auth_commands.rb'
43
53
  - 'lib/mysigner/cli/build_commands.rb'
44
54
  - 'lib/mysigner/cli/diagnostic_commands.rb'
@@ -57,7 +67,7 @@ Metrics/ClassLength:
57
67
  Exclude:
58
68
  - 'lib/mysigner/signing/wizard.rb'
59
69
 
60
- # Offense count: 25
70
+ # Offense count: 26
61
71
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
62
72
  Metrics/CyclomaticComplexity:
63
73
  Exclude:
@@ -68,8 +78,9 @@ Metrics/CyclomaticComplexity:
68
78
  - 'lib/mysigner/cli/resource_commands.rb'
69
79
  - 'lib/mysigner/cli/validate_commands.rb'
70
80
  - 'lib/mysigner/signing/wizard.rb'
81
+ - 'lib/mysigner/upload/app_store_automation.rb'
71
82
 
72
- # Offense count: 43
83
+ # Offense count: 45
73
84
  # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
74
85
  Metrics/MethodLength:
75
86
  Exclude:
@@ -92,14 +103,16 @@ Metrics/ModuleLength:
92
103
  - 'lib/mysigner/cli/diagnostic_commands.rb'
93
104
  - 'lib/mysigner/cli/resource_commands.rb'
94
105
 
95
- # Offense count: 2
106
+ # Offense count: 5
96
107
  # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
97
108
  Metrics/ParameterLists:
98
109
  Exclude:
99
110
  - 'lib/mysigner/build/executor.rb'
100
111
  - 'lib/mysigner/signing/keystore_manager.rb'
112
+ - 'lib/mysigner/upload/asc_rest_uploader.rb'
113
+ - 'lib/mysigner/upload/play_store_uploader.rb'
101
114
 
102
- # Offense count: 26
115
+ # Offense count: 27
103
116
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
104
117
  Metrics/PerceivedComplexity:
105
118
  Exclude:
@@ -110,3 +123,4 @@ Metrics/PerceivedComplexity:
110
123
  - 'lib/mysigner/cli/resource_commands.rb'
111
124
  - 'lib/mysigner/cli/validate_commands.rb'
112
125
  - 'lib/mysigner/signing/wizard.rb'
126
+ - 'lib/mysigner/upload/app_store_automation.rb'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mysigner (0.1.3)
4
+ mysigner (0.1.5)
5
5
  base64 (~> 0.2)
6
6
  faraday (~> 2.14)
7
7
  faraday-retry (~> 2.2)
data/README.md CHANGED
@@ -544,7 +544,7 @@ For questions or issues:
544
544
 
545
545
  ## License
546
546
 
547
- Copyright 2025 Jurgen Leka
547
+ Copyright 2025 MySigner
548
548
 
549
549
  Licensed under the Apache License, Version 2.0 (the "License");
550
550
  you may not use this file except in compliance with the License.
data/certificate_.cer ADDED
File without changes
data/exe/mysigner CHANGED
@@ -3,4 +3,20 @@
3
3
 
4
4
  require 'mysigner'
5
5
 
6
- Mysigner::CLI.start(ARGV)
6
+ # Normalize `mysigner <cmd> [args…] --help` → `mysigner help <cmd>`. Thor
7
+ # treats extra positional args as a fatal argument-count error, so without
8
+ # this rewrite users who type `mysigner ship testflight --help` see
9
+ # `ERROR: "mysigner ship" was called with arguments ["testflight", "--help"]`
10
+ # instead of the usage docs. Rewriting at the entry point keeps every
11
+ # subcommand's `--help` behaviour predictable.
12
+ def rewrite_help_flag!(argv)
13
+ return argv unless argv.any? { |a| ['--help', '-h'].include?(a) }
14
+
15
+ # Don't touch `help` itself or `--version` / bare `-v` invocations.
16
+ first = argv.find { |a| !a.start_with?('-') }
17
+ return argv if first.nil? || first == 'help'
18
+
19
+ ['help', first]
20
+ end
21
+
22
+ Mysigner::CLI.start(rewrite_help_flag!(ARGV))
@@ -0,0 +1 @@
1
+ Server error
@@ -0,0 +1 @@
1
+ Server error
@@ -91,11 +91,23 @@ module Mysigner
91
91
  # Handle framework-specific pre-build steps
92
92
  run_pre_build_steps
93
93
 
94
- # Build command with signing properties
95
- cmd = build_gradle_command(task)
94
+ # Phase 0: inject signing via Gradle init-script + env vars. Passwords
95
+ # never appear in argv (no -Pandroid.injected.signing.*=PLAINTEXT).
96
+ injector = nil
97
+ if @keystore_path && File.exist?(@keystore_path)
98
+ require 'mysigner/signing/gradle_signing_injector'
99
+ injector = Mysigner::Signing::GradleSigningInjector.new
100
+ @signing_init_script_path = injector.write_init_script!
101
+ end
96
102
 
97
- # Execute build
98
- execute_with_output(cmd)
103
+ begin
104
+ # Build command with signing properties referencing the init script
105
+ cmd = build_gradle_command(task)
106
+ execute_with_output(cmd)
107
+ ensure
108
+ injector&.cleanup!
109
+ @signing_init_script_path = nil
110
+ end
99
111
  end
100
112
 
101
113
  def ensure_java_home!
@@ -233,20 +245,34 @@ module Mysigner
233
245
  cmd_parts << '&&'
234
246
  end
235
247
 
248
+ # Phase 0: export signing env vars inline so they're only visible to
249
+ # the child process, not in argv. The Gradle init script below reads
250
+ # MYSIGNER_STORE_FILE / MYSIGNER_STORE_PASSWORD / MYSIGNER_KEY_ALIAS /
251
+ # MYSIGNER_KEY_PASSWORD and configures signingConfigs.release.
252
+ if @keystore_path && File.exist?(@keystore_path) && @signing_init_script_path
253
+ cmd_parts << "export MYSIGNER_STORE_FILE=#{shell_escape(File.absolute_path(@keystore_path))}"
254
+ cmd_parts << '&&'
255
+ cmd_parts << "export MYSIGNER_STORE_PASSWORD=#{shell_escape(@keystore_password)}" if @keystore_password
256
+ cmd_parts << '&&' if @keystore_password
257
+ cmd_parts << "export MYSIGNER_KEY_ALIAS=#{shell_escape(@key_alias)}" if @key_alias
258
+ cmd_parts << '&&' if @key_alias
259
+ cmd_parts << "export MYSIGNER_KEY_PASSWORD=#{shell_escape(@key_password)}" if @key_password
260
+ cmd_parts << '&&' if @key_password
261
+ end
262
+
236
263
  # Change to android directory and run gradle
237
264
  cmd_parts << "cd #{shell_escape(android_dir)}"
238
265
  cmd_parts << '&&'
239
266
  cmd_parts << gradle_cmd
240
- cmd_parts << task
241
267
 
242
- # Add signing properties if provided
243
- if @keystore_path && File.exist?(@keystore_path)
244
- cmd_parts << "-Pandroid.injected.signing.store.file=#{shell_escape(File.absolute_path(@keystore_path))}"
245
- cmd_parts << "-Pandroid.injected.signing.store.password=#{shell_escape(@keystore_password)}" if @keystore_password
246
- cmd_parts << "-Pandroid.injected.signing.key.alias=#{shell_escape(@key_alias)}" if @key_alias
247
- cmd_parts << "-Pandroid.injected.signing.key.password=#{shell_escape(@key_password)}" if @key_password
268
+ # Reference the init script (contains the signing-config override)
269
+ if @signing_init_script_path
270
+ cmd_parts << '--init-script'
271
+ cmd_parts << shell_escape(@signing_init_script_path)
248
272
  end
249
273
 
274
+ cmd_parts << task
275
+
250
276
  # Add version code override if provided (no file modification needed)
251
277
  cmd_parts << "-PversionCode=#{@version_code}" if @version_code
252
278
 
@@ -46,7 +46,11 @@ module Mysigner
46
46
  # Execute build
47
47
  success = execute_with_output(cmd)
48
48
 
49
- raise BuildError, 'Build failed. Check output above for errors.' unless success
49
+ unless success
50
+ msg = +'Build failed.'
51
+ msg << " Full log: #{@last_build_log}" if @last_build_log
52
+ raise BuildError, msg
53
+ end
50
54
 
51
55
  # Verify archive was created
52
56
  raise BuildError, "Build reported success but archive not found at: #{archive_path}" unless File.exist?(archive_path)
@@ -129,40 +133,76 @@ module Mysigner
129
133
  puts ''
130
134
 
131
135
  @build_errors = []
132
-
133
- # Run command and capture output in real-time
134
- IO.popen(cmd, err: %i[child out]) do |io|
135
- io.each_line do |line|
136
- # Filter output to show only important messages
137
- next if line.strip.empty?
138
-
139
- # Detect error lines (case-insensitive for error:)
140
- # Check for various error patterns including curly quotes from Xcode
141
- is_error = line.downcase.include?('error:') ||
142
- line.include?('Provisioning profile') ||
143
- line.include?('Code Sign error') ||
144
- line.include?("doesn't support") ||
145
- line.include?("doesn\u2019t support") ||
146
- line.include?('capability')
147
-
148
- is_warning = line.downcase.include?('warning:')
149
-
150
- # Show and capture errors and warnings
151
- if is_error || is_warning
152
- puts line
153
- @build_errors << line if is_error
154
- # Show progress markers
155
- elsif line.include?('Building') || line.include?('Compiling') ||
156
- line.include?('Linking') || line.include?('Signing') ||
157
- line.include?('Copying')
158
- print '.'
136
+ @last_build_log = log_path_for_run
137
+
138
+ # Capture every line so we can replay it on failure. xcodebuild's
139
+ # `-quiet` plus our keyword filter happily hides framework-loader
140
+ # errors, license issues, and anything that doesn't say "error:" —
141
+ # leaving the user with "Build failed" and nothing actionable. The
142
+ # log file (and tail dump on failure) keeps the full output recoverable.
143
+ File.open(@last_build_log, 'w') do |log|
144
+ # Run command and capture output in real-time
145
+ IO.popen(cmd, err: %i[child out]) do |io|
146
+ io.each_line do |line|
147
+ log.write(line)
148
+
149
+ # Filter output to show only important messages
150
+ next if line.strip.empty?
151
+
152
+ # Detect error lines (case-insensitive for error:)
153
+ # Check for various error patterns including curly quotes from Xcode
154
+ is_error = line.downcase.include?('error:') ||
155
+ line.include?('Provisioning profile') ||
156
+ line.include?('Code Sign error') ||
157
+ line.include?("doesn't support") ||
158
+ line.include?("doesn\u2019t support") ||
159
+ line.include?('capability')
160
+
161
+ is_warning = line.downcase.include?('warning:')
162
+
163
+ # Show and capture errors and warnings
164
+ if is_error || is_warning
165
+ puts line
166
+ @build_errors << line if is_error
167
+ # Show progress markers
168
+ elsif line.include?('Building') || line.include?('Compiling') ||
169
+ line.include?('Linking') || line.include?('Signing') ||
170
+ line.include?('Copying')
171
+ print '.'
172
+ end
159
173
  end
160
174
  end
161
175
  end
162
176
 
163
177
  puts '' # New line after dots
164
178
 
165
- $CHILD_STATUS.success?
179
+ success = $CHILD_STATUS.success?
180
+ dump_log_tail(@last_build_log) unless success
181
+ success
182
+ end
183
+
184
+ def log_path_for_run
185
+ log_dir = File.join(@project_info[:directory], 'build')
186
+ FileUtils.mkdir_p(log_dir)
187
+ File.join(log_dir, 'last-build.log')
188
+ end
189
+
190
+ def dump_log_tail(path, lines: 80)
191
+ return unless File.exist?(path)
192
+
193
+ tail = File.foreach(path).each_with_object([]) do |line, buf|
194
+ buf << line
195
+ buf.shift if buf.size > lines
196
+ end
197
+ return if tail.empty?
198
+
199
+ puts ''
200
+ puts '─' * 80
201
+ puts "Build output (last #{tail.size} lines):"
202
+ puts '─' * 80
203
+ puts tail.join
204
+ puts '─' * 80
205
+ puts "Full log: #{path}"
166
206
  end
167
207
  end
168
208
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'xcodeproj'
4
-
5
3
  module Mysigner
6
4
  module Build
7
5
  class Parser
@@ -184,6 +182,8 @@ module Mysigner
184
182
  end
185
183
 
186
184
  def open_project
185
+ require 'xcodeproj'
186
+
187
187
  if @project_info[:type] == :workspace
188
188
  # Workspace contains multiple projects
189
189
  # Get the main project (not Pods)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Mysigner
6
+ module Cleanup
7
+ class PrivateKeysPurger
8
+ LEGACY_DIRS = [
9
+ '~/.private_keys',
10
+ '~/.appstoreconnect/private_keys'
11
+ ].freeze
12
+
13
+ def marker_path
14
+ File.expand_path('~/.mysigner/.private_keys_purged')
15
+ end
16
+
17
+ def call
18
+ return if ENV['MYSIGNER_USE_LEGACY_ASC'] == '1'
19
+ return if File.exist?(marker_path)
20
+
21
+ LEGACY_DIRS.each do |dir|
22
+ expanded = File.expand_path(dir)
23
+ Dir.glob(File.join(expanded, 'AuthKey_*.p8')).each do |path|
24
+ File.delete(path)
25
+ warn "mysigner: deleted legacy private key #{path}" if ENV['MYSIGNER_VERBOSE'] == '1'
26
+ rescue Errno::ENOENT
27
+ # Raced with another process — already gone.
28
+ rescue Errno::EACCES, Errno::EPERM => e
29
+ # Owned by another user. Skip this file but keep going so the
30
+ # marker still gets written — otherwise the purger retries on
31
+ # every CLI invocation and keeps failing on the same file.
32
+ warn "mysigner: could not delete #{path} (#{e.class}); skipping" if ENV['MYSIGNER_VERBOSE'] == '1'
33
+ end
34
+ end
35
+
36
+ FileUtils.mkdir_p(File.dirname(marker_path))
37
+ FileUtils.touch(marker_path)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -5,7 +5,7 @@ module Mysigner
5
5
  module AuthCommands
6
6
  def self.included(base)
7
7
  base.class_eval do
8
- desc 'version', 'Show CLI version and system information'
8
+ desc 'version', 'Show version information'
9
9
  def version
10
10
  say "My Signer CLI v#{Mysigner::VERSION}", :cyan
11
11
  say ''
@@ -13,6 +13,9 @@ module Mysigner
13
13
  say "Install: #{File.expand_path('../../..', __dir__)}", :white
14
14
  say "Config: #{Config::CONFIG_FILE}", :white
15
15
  say ''
16
+ say 'Repository: https://github.com/mysigner-dev/mysigner-cli', :white
17
+ say 'Issues: https://github.com/mysigner-dev/mysigner-cli/issues', :white
18
+ say ''
16
19
  say 'Docs: https://mysigner.dev/docs/commands', :white
17
20
  say 'Support: https://mysigner.dev/landing#contact', :white
18
21
  end
@@ -738,10 +741,14 @@ module Mysigner
738
741
  end
739
742
  end
740
743
 
741
- desc 'switch', 'Switch between organizations (for multi-org users)'
744
+ desc 'switch [ORG_ID]', 'Switch between organizations (for multi-org users)'
742
745
  long_desc <<~DESC
743
746
  Switch to a different organization.
744
747
 
748
+ Interactive (no argument): prompts you with a numbered list.
749
+ Non-interactive: pass an organization ID directly, e.g.
750
+ mysigner switch 7
751
+
745
752
  With organization-specific tokens, you'll need a token for each
746
753
  organization you want to access. This command will:
747
754
  - Show all organizations you're a member of
@@ -753,7 +760,7 @@ module Mysigner
753
760
  Note: You need to be the same user in all organizations. Tokens
754
761
  from different user accounts will be rejected.
755
762
  DESC
756
- def switch
763
+ def switch(target_org_id = nil)
757
764
  config = load_config
758
765
  client = create_client(config)
759
766
 
@@ -815,20 +822,31 @@ module Mysigner
815
822
  say 'Legend: ✓ = Has token | ⚠️ = Needs token', :white
816
823
  say ''
817
824
 
818
- # Let user select
819
- org_index = ask("Select organization (1-#{organizations_list.length}, or 'q' to cancel):")
820
-
821
- if org_index.downcase == 'q'
822
- say 'Cancelled', :yellow
823
- return
824
- end
825
-
826
- unless org_index.match(/^\d+$/) && org_index.to_i.between?(1, organizations_list.length)
827
- error 'Invalid selection'
828
- return
829
- end
830
-
831
- selected_org = organizations_list[org_index.to_i - 1]
825
+ # Non-interactive selection via positional arg (`mysigner switch 7`)
826
+ selected_org = if target_org_id
827
+ match = organizations_list.find { |o| o[:id].to_s == target_org_id.to_s }
828
+ unless match
829
+ error "Organization #{target_org_id} not found among your memberships"
830
+ say ''
831
+ say " Available IDs: #{organizations_list.map { |o| o[:id] }.join(', ')}", :yellow
832
+ exit 1
833
+ end
834
+ match
835
+ else
836
+ org_index = ask("Select organization (1-#{organizations_list.length}, or 'q' to cancel):")
837
+
838
+ if org_index.downcase == 'q'
839
+ say 'Cancelled', :yellow
840
+ return
841
+ end
842
+
843
+ unless org_index.match(/^\d+$/) && org_index.to_i.between?(1, organizations_list.length)
844
+ error 'Invalid selection'
845
+ return
846
+ end
847
+
848
+ organizations_list[org_index.to_i - 1]
849
+ end
832
850
 
833
851
  if selected_org[:id] == config.current_organization_id
834
852
  say ''
@@ -941,8 +959,14 @@ module Mysigner
941
959
  end
942
960
 
943
961
  no_commands do
944
- # Helper method for yes/no prompts with Enter defaulting to yes
962
+ # Helper method for yes/no prompts with Enter defaulting to yes.
963
+ # Defaults to NO when stdin is not a TTY so automation (CI, pipes)
964
+ # never silently opts-in to mutating operations.
945
965
  def yes_with_default?(statement, color = nil)
966
+ unless $stdin.tty?
967
+ say "#{statement} [Y/n] (non-interactive: assuming no)", color
968
+ return false
969
+ end
946
970
  response = ask("#{statement} [Y/n]", color).to_s.strip.downcase
947
971
  response.empty? || response == 'y' || response == 'yes'
948
972
  end