mysigner 0.3.0 → 0.3.2

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.
@@ -1393,6 +1393,8 @@ module Mysigner
1393
1393
 
1394
1394
  mysigner android build
1395
1395
  Build an AAB file for upload to Google Play Console.
1396
+ (An AAB — Android App Bundle, .aab — is the format Google Play
1397
+ requires; the CLI builds and signs it for you, not an APK.)
1396
1398
  Use this for your FIRST upload (required before mysigner ship works).
1397
1399
 
1398
1400
  mysigner android list
@@ -1421,6 +1423,13 @@ module Mysigner
1421
1423
  DESC
1422
1424
  method_option :name, type: :string, desc: 'Display name for the app'
1423
1425
  def android(action, *args)
1426
+ # mysigner-22 follow-up — `android build` is the only LOCAL action;
1427
+ # init/add/list all manage MySigner-registered records. Gate the
1428
+ # rest at the dispatcher top so users see "android add" in the
1429
+ # banner (not the underlying "apps" call that `list` invokes) and
1430
+ # so `android add` doesn't crash with NoMethodError on the nil
1431
+ # client further down.
1432
+ exit_unless_local_supported!("android #{action}") unless %w[build help].include?(action)
1424
1433
  config = load_config
1425
1434
  client = create_client(config)
1426
1435
 
@@ -1724,8 +1733,12 @@ module Mysigner
1724
1733
  if keystore_info
1725
1734
  say "🔐 Keystore: #{keystore_info[:name]}", :green
1726
1735
  else
1727
- say '⚠️ No keystore configured - will use debug signing', :yellow
1728
- say " Run 'mysigner android init' to set up release signing", :yellow
1736
+ say '⚠️ No release keystore configured building a DEBUG-SIGNED AAB.', :yellow
1737
+ say ' A debug-signed AAB is fine for local install/testing but Google', :yellow
1738
+ say ' Play will REJECT it on upload. To produce a publishable build:', :yellow
1739
+ say ' • Vault mode: mysigner android init (set up release signing)', :yellow
1740
+ say ' • Local-only mode: pass --keystore-path / --keystore-password to', :yellow
1741
+ say ' `mysigner ship … --platform android`', :yellow
1729
1742
  end
1730
1743
  say ''
1731
1744
  say '⏱️ This may take a few minutes...', :yellow
@@ -1771,16 +1784,21 @@ module Mysigner
1771
1784
  say ' mysigner ship internal --platform android', :green
1772
1785
  say ''
1773
1786
 
1774
- # Open the folder containing the AAB
1775
- aab_dir = File.dirname(aab_path)
1776
- say '📂 Opening folder...', :yellow
1777
- case RUBY_PLATFORM
1778
- when /darwin/
1779
- system('open', aab_dir)
1780
- when /linux/
1781
- system('xdg-open', aab_dir)
1782
- when /mingw|mswin/
1783
- system('explorer', aab_dir.gsub('/', '\\'))
1787
+ # Open the folder containing the AAB — only in an interactive
1788
+ # terminal. On headless / CI / SSH there is no file manager, and
1789
+ # xdg-open/explorer would spew errors AFTER a successful build.
1790
+ if $stdout.tty?
1791
+ aab_dir = File.dirname(aab_path)
1792
+ opener = case RUBY_PLATFORM
1793
+ when /darwin/ then 'open'
1794
+ when /linux/ then 'xdg-open'
1795
+ when /mingw|mswin/ then 'explorer'
1796
+ end
1797
+ if opener
1798
+ say '📂 Opening folder...', :yellow
1799
+ target = opener == 'explorer' ? aab_dir.gsub('/', '\\') : aab_dir
1800
+ system(opener, target, %i[out err] => File::NULL)
1801
+ end
1784
1802
  end
1785
1803
  rescue Build::Detector::NoProjectError => e
1786
1804
  error e.message
@@ -1892,12 +1910,19 @@ module Mysigner
1892
1910
  # Write modified app.json
1893
1911
  File.write(app_json_path, JSON.pretty_generate(config))
1894
1912
 
1895
- begin
1896
- # Delete existing android folder
1897
-
1898
- FileUtils.rm_rf(android_dir)
1913
+ # Back up an existing android/ so a failed prebuild can't destroy a
1914
+ # committed or hand-edited native project — restored on any failure.
1915
+ backup_dir = nil
1916
+ if Dir.exist?(android_dir)
1917
+ # pid + timestamp so a crashed prior run (same pid reused) can't
1918
+ # collide and have its backup wiped by the rm_rf below.
1919
+ backup_dir = "#{android_dir}.mysigner-bak-#{Process.pid}-#{Time.now.to_i}"
1920
+ FileUtils.rm_rf(backup_dir)
1921
+ FileUtils.mv(android_dir, backup_dir)
1922
+ end
1899
1923
 
1900
- # Run expo prebuild
1924
+ begin
1925
+ # Regenerate the native android/ from the version-bumped config.
1901
1926
  Dir.chdir(project_dir) do
1902
1927
  success = system('npx', 'expo', 'prebuild', '--platform', 'android', '--clean')
1903
1928
  raise 'expo prebuild failed' unless success
@@ -1912,8 +1937,28 @@ module Mysigner
1912
1937
  File.expand_path('~/Library/Android/sdk')
1913
1938
  File.write(local_props_path, "sdk.dir=#{sdk_path}\n") if Dir.exist?(sdk_path)
1914
1939
  end
1940
+
1941
+ # Success — drop the backup of the old android/.
1942
+ FileUtils.rm_rf(backup_dir) if backup_dir
1943
+ rescue StandardError
1944
+ # Restore the original android/ so a failed regeneration never
1945
+ # leaves the user worse off than before.
1946
+ if backup_dir && Dir.exist?(backup_dir)
1947
+ begin
1948
+ FileUtils.rm_rf(android_dir)
1949
+ FileUtils.mv(backup_dir, android_dir)
1950
+ rescue StandardError => restore_err
1951
+ # Don't let a restore failure silently strand the user's
1952
+ # native project inside the backup dir — tell them exactly
1953
+ # where it is and how to recover it.
1954
+ say "⚠️ Could not auto-restore your android/ folder: #{restore_err.message}", :red
1955
+ say " Your original android/ is preserved at: #{backup_dir}", :yellow
1956
+ say " Recover it with: mv '#{backup_dir}' '#{android_dir}'", :yellow
1957
+ end
1958
+ end
1959
+ raise
1915
1960
  ensure
1916
- # Restore original app.json
1961
+ # Always restore the original app.json
1917
1962
  File.write(app_json_path, original_content)
1918
1963
  end
1919
1964
  end
@@ -2109,17 +2154,23 @@ module Mysigner
2109
2154
  gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all']
2110
2155
  gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
2111
2156
 
2157
+ # Write the init script when we need to inject signing OR a
2158
+ # versionCode override (a bare -PversionCode is ignored by stock
2159
+ # build.gradle, so versionCode must flow through the init script).
2112
2160
  injector = nil
2113
2161
  env = {}
2114
- if keystore_info
2162
+ if keystore_info || version_code_override
2115
2163
  injector = Mysigner::Signing::GradleSigningInjector.new
2116
2164
  init_path = injector.write_init_script!
2117
- env = keystore_info[:signing_env_vars] || injector.env_vars(
2118
- keystore_path: keystore_info[:path],
2119
- store_password: keystore_info[:password],
2120
- key_password: keystore_info[:key_password],
2121
- key_alias: keystore_info[:key_alias]
2122
- )
2165
+ if keystore_info
2166
+ env = keystore_info[:signing_env_vars] || injector.env_vars(
2167
+ keystore_path: keystore_info[:path],
2168
+ store_password: keystore_info[:password],
2169
+ key_password: keystore_info[:key_password],
2170
+ key_alias: keystore_info[:key_alias]
2171
+ )
2172
+ end
2173
+ env['MYSIGNER_VERSION_CODE'] = version_code_override.to_s if version_code_override
2123
2174
  gradle_args.insert(1, '--init-script', init_path)
2124
2175
  end
2125
2176
 
@@ -2138,9 +2189,10 @@ module Mysigner
2138
2189
  # Find the AAB
2139
2190
  aab_path = File.join(android_dir, 'app/build/outputs/bundle/release/app-release.aab')
2140
2191
  unless File.exist?(aab_path)
2141
- # Try alternate paths
2192
+ # Fall back to the NEWEST matching AAB so a stale/wrong-flavor
2193
+ # artifact from a previous build is never returned.
2142
2194
  alt_paths = Dir.glob(File.join(android_dir, 'app/build/outputs/bundle/*/*.aab'))
2143
- aab_path = alt_paths.first if alt_paths.any?
2195
+ aab_path = alt_paths.max_by { |f| File.mtime(f) } if alt_paths.any?
2144
2196
  end
2145
2197
  aab_path
2146
2198
  end
@@ -147,7 +147,22 @@ module Mysigner
147
147
  say 'Running local Signing::Validator (server checks skipped in local-only mode).', :yellow
148
148
  say ''
149
149
 
150
- project_info = Mysigner::Build::Detector.detect
150
+ # Read-only detection: never run an expo prebuild from `validate`,
151
+ # and turn a genuine "no project here" into a clean error instead
152
+ # of a raw NoProjectError backtrace.
153
+ project_info = begin
154
+ Mysigner::Build::Detector.detect(allow_prebuild: false)
155
+ rescue Mysigner::Build::Detector::NoProjectError => e
156
+ error e.message
157
+ exit 1
158
+ end
159
+
160
+ if project_info[:needs_prebuild]
161
+ say 'ℹ️ Expo managed project detected — no native iOS project generated yet.', :yellow
162
+ say ' Run `mysigner ship` (or `npx expo prebuild`) to create it, then re-run validate.', :yellow
163
+ return
164
+ end
165
+
151
166
  parser = Mysigner::Build::Parser.new(project_info)
152
167
  target_name = parser.main_target.name
153
168
 
@@ -72,7 +72,15 @@ module Mysigner
72
72
  # Local-only mode at the Config level: cascade ENV → file. The CLI
73
73
  # Helpers concern layers --local-only / --no-local-only on top.
74
74
  def self.local_only?
75
- truthy_env?(ENV_LOCAL_ONLY) || local_only_from_file?
75
+ local_only_from_env? || local_only_from_file?
76
+ end
77
+
78
+ # Public predicate for the env-var source. Mirrors local_only_from_file?
79
+ # so status's "Source: …" attribution can distinguish env vs file
80
+ # using the same truthy parser the cascade uses (a literal env value
81
+ # of "0" / "false" reads as off, not as "env var enabled it").
82
+ def self.local_only_from_env?
83
+ truthy_env?(ENV_LOCAL_ONLY)
76
84
  end
77
85
 
78
86
  # Lightweight check that reads only ~/.mysigner/config.yml's
@@ -243,13 +251,25 @@ module Mysigner
243
251
  # Display config (with masked tokens)
244
252
  def display
245
253
  current_org_name = org_name(@current_organization_id) || '(not set)'
246
- current_token = api_token(@current_organization_id)
254
+ # Never let an undecryptable token turn `mysigner config` into a crash —
255
+ # render it as an actionable placeholder instead.
256
+ current_token = begin
257
+ api_token(@current_organization_id)
258
+ rescue ConfigError
259
+ :unreadable
260
+ end
261
+
262
+ token_display = case current_token
263
+ when nil then '(not set)'
264
+ when :unreadable then '(unreadable — run mysigner login)'
265
+ else mask_token(current_token)
266
+ end
247
267
 
248
268
  display_data = {
249
269
  api_url: @api_url || '(not set)',
250
270
  user_email: @user_email || '(not set)',
251
271
  current_organization: "#{current_org_name} (ID: #{@current_organization_id || 'not set'})",
252
- current_token: current_token ? mask_token(current_token) : '(not set)'
272
+ current_token: token_display
253
273
  }
254
274
 
255
275
  # Show all organizations
@@ -14,6 +14,20 @@ module Mysigner
14
14
  allprojects {
15
15
  afterEvaluate { project ->
16
16
  if (!project.hasProperty('android')) return
17
+
18
+ // versionCode override (MySigner auto-increment). Applied here so it
19
+ // actually takes effect even when app/build.gradle hard-codes
20
+ // versionCode in defaultConfig — a plain -PversionCode project
21
+ // property is silently ignored by stock build.gradle files.
22
+ // Gate to the APPLICATION module only: com.android.library modules
23
+ // have no versionCode in the AGP 7/8 library DSL, so assigning it
24
+ // there raises. allprojects+afterEvaluate fires for every Android
25
+ // subproject, hence the explicit application-plugin check.
26
+ def vcRaw = System.getenv('MYSIGNER_VERSION_CODE')
27
+ if (vcRaw && vcRaw.isInteger() && project.plugins.hasPlugin('com.android.application')) {
28
+ project.android.defaultConfig.versionCode = vcRaw.toInteger()
29
+ }
30
+
17
31
  def storePw = System.getenv('MYSIGNER_STORE_PASSWORD')
18
32
  def keyPw = System.getenv('MYSIGNER_KEY_PASSWORD')
19
33
  def aliasName = System.getenv('MYSIGNER_KEY_ALIAS')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mysigner
4
- VERSION = '0.3.0'
4
+ VERSION = '0.3.2'
5
5
  end
data/mysigner.gemspec CHANGED
@@ -38,14 +38,19 @@ Gem::Specification.new do |spec|
38
38
 
39
39
  \e[32m✓\e[0m You're ready to automate iOS & Android code signing.
40
40
 
41
- \e[35mNext steps:\e[0m
42
- • \e[33mRun\e[0m \e[1m`mysigner onboard`\e[0m Guided first-time setup (API URL, org, token)
43
- \e[33mRun\e[0m \e[1m`mysigner login`\e[0m – Skip onboarding if you already have a token
44
- • \e[33mRun\e[0m \e[1m`mysigner doctor`\e[0m – Validate your development environment
45
- \e[33mRun\e[0m \e[1m`mysigner help`\e[0m – Explore every command in the toolbox
46
-
47
- \e[35miOS:\e[0m mysigner ship testflight
48
- \e[35mAndroid:\e[0m mysigner ship internal --platform android
41
+ \e[35mTwo ways to use it:\e[0m
42
+ • \e[1mWith a free My Signer account\e[0m (keys stored & synced for you):
43
+ \e[33mRun\e[0m \e[1m`mysigner onboard`\e[0m
44
+ • \e[1mNo account — your keys stay on this machine\e[0m (nothing sent to a server):
45
+ \e[33mRun\e[0m \e[1m`mysigner --local-only onboard`\e[0m
46
+ \e[33mNot sure? Start with --local-only.\e[0m
47
+
48
+ \e[35mAlso handy:\e[0m
49
+ • \e[33mRun\e[0m \e[1m`mysigner doctor`\e[0m – Check your dev environment (JDK, SDK, Xcode…)
50
+ • \e[33mRun\e[0m \e[1m`mysigner help`\e[0m – Explore every command
51
+
52
+ \e[35miOS (needs a Mac):\e[0m mysigner ship testflight
53
+ \e[35mAndroid:\e[0m mysigner ship internal --platform android
49
54
 
50
55
  \e[36mDocs:\e[0m https://mysigner.dev/docs/commands
51
56
  MSG
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysigner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jurgen Leka
@@ -287,14 +287,15 @@ metadata:
287
287
  rubygems_mfa_required: 'true'
288
288
  post_install_message: "\e[36m╔══════════════════════════════════════════════════════════════╗\e[0m\n\e[36m║
289
289
  \e[1m\U0001F680 Welcome to My Signer CLI\e[0m\e[36m ║\e[0m\n\e[36m╚══════════════════════════════════════════════════════════════╝\e[0m\n\n\e[32m✓\e[0m
290
- \ You're ready to automate iOS & Android code signing.\n\n\e[35mNext steps:\e[0m\n
291
- \ • \e[33mRun\e[0m \e[1m`mysigner onboard`\e[0m – Guided first-time setup (API
292
- URL, org, token)\n\e[33mRun\e[0m \e[1m`mysigner login`\e[0m Skip onboarding
293
- if you already have a token\n • \e[33mRun\e[0m \e[1m`mysigner doctor`\e[0m
294
- Validate your development environment\n\e[33mRun\e[0m \e[1m`mysigner help`\e[0m
295
- \ Explore every command in the toolbox\n\n\e[35miOS:\e[0m mysigner
296
- ship testflight\n\e[35mAndroid:\e[0m mysigner ship internal --platform android\n\n\e[36mDocs:\e[0m
297
- https://mysigner.dev/docs/commands\n"
290
+ \ You're ready to automate iOS & Android code signing.\n\n\e[35mTwo ways to use
291
+ it:\e[0m\n • \e[1mWith a free My Signer account\e[0m (keys stored & synced for
292
+ you):\n \e[33mRun\e[0m \e[1m`mysigner onboard`\e[0m\n • \e[1mNo account —
293
+ your keys stay on this machine\e[0m (nothing sent to a server):\n \e[33mRun\e[0m
294
+ \e[1m`mysigner --local-only onboard`\e[0m\n \e[33mNot sure? Start with --local-only.\e[0m\n\n\e[35mAlso
295
+ handy:\e[0m\n • \e[33mRun\e[0m \e[1m`mysigner doctor`\e[0m – Check your dev environment
296
+ (JDK, SDK, Xcode…)\n\e[33mRun\e[0m \e[1m`mysigner help`\e[0m – Explore every
297
+ command\n\n\e[35miOS (needs a Mac):\e[0m mysigner ship testflight\n\e[35mAndroid:\e[0m
298
+ \ mysigner ship internal --platform android\n\n\e[36mDocs:\e[0m https://mysigner.dev/docs/commands\n"
298
299
  rdoc_options: []
299
300
  require_paths:
300
301
  - lib
@@ -309,7 +310,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
309
310
  - !ruby/object:Gem::Version
310
311
  version: '0'
311
312
  requirements: []
312
- rubygems_version: 4.0.11
313
+ rubygems_version: 3.6.9
313
314
  specification_version: 4
314
315
  summary: CLI tool for iOS and Android code signing automation via My Signer API
315
316
  test_files: []