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 +4 -4
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +23 -9
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/certificate_.cer +0 -0
- data/exe/mysigner +17 -1
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +37 -11
- data/lib/mysigner/build/executor.rb +69 -29
- data/lib/mysigner/build/parser.rb +2 -2
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +42 -18
- data/lib/mysigner/cli/build_commands.rb +307 -117
- data/lib/mysigner/cli/concerns/helpers.rb +32 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +8 -1
- data/lib/mysigner/cli/resource_commands.rb +304 -114
- data/lib/mysigner/cli.rb +8 -0
- data/lib/mysigner/config.rb +68 -4
- data/lib/mysigner/export/exporter.rb +6 -1
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +50 -25
- data/lib/mysigner/upload/app_store_automation.rb +46 -1
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +77 -40
- data/lib/mysigner/upload/uploader.rb +41 -12
- data/lib/mysigner/version.rb +1 -1
- data/profile_.mobileprovision +0 -0
- metadata +9 -3
- data/.DS_Store +0 -0
|
@@ -144,6 +144,23 @@ module Mysigner
|
|
|
144
144
|
udid = args[1]
|
|
145
145
|
platform = options[:platform].upcase
|
|
146
146
|
|
|
147
|
+
# Client-side UDID sanity check — catches obvious typos and
|
|
148
|
+
# copy-paste errors before they hit Apple's API (which, for IOS
|
|
149
|
+
# at least, has historically accepted synthetic/all-zeros UDIDs
|
|
150
|
+
# in sandbox environments). Skipped for non-IOS platforms.
|
|
151
|
+
if (platform == 'IOS') && !valid_ios_udid?(udid)
|
|
152
|
+
error "Invalid iOS UDID: #{udid}"
|
|
153
|
+
say ''
|
|
154
|
+
say 'iOS UDIDs must:', :yellow
|
|
155
|
+
say ' • Be either 25 chars (older devices) or 40 hex chars (newer)', :cyan
|
|
156
|
+
say ' • Be hexadecimal (0-9, A-F)', :cyan
|
|
157
|
+
say ' • Not be trivially synthetic (e.g. all zeros, single repeated char)', :cyan
|
|
158
|
+
say ''
|
|
159
|
+
say '💡 Get a real UDID:', :cyan
|
|
160
|
+
say ' mysigner device detect', :yellow
|
|
161
|
+
exit 1
|
|
162
|
+
end
|
|
163
|
+
|
|
147
164
|
say '📱 Registering device...', :cyan
|
|
148
165
|
say ''
|
|
149
166
|
|
|
@@ -296,7 +313,11 @@ module Mysigner
|
|
|
296
313
|
|
|
297
314
|
# Also try xcrun xctrace (Xcode command line tools)
|
|
298
315
|
if devices.empty? && system('which xcrun > /dev/null 2>&1')
|
|
299
|
-
output
|
|
316
|
+
# xctrace output can contain non-ASCII bytes (emoji in device
|
|
317
|
+
# names, accented characters). Force UTF-8 + scrub invalid
|
|
318
|
+
# sequences so `.strip` / `.each_line` don't raise
|
|
319
|
+
# Encoding::CompatibilityError under the default US-ASCII locale.
|
|
320
|
+
output = `xcrun xctrace list devices 2>/dev/null`.force_encoding('UTF-8').scrub
|
|
300
321
|
in_devices_section = false
|
|
301
322
|
|
|
302
323
|
output.each_line do |line|
|
|
@@ -591,14 +612,18 @@ module Mysigner
|
|
|
591
612
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/profiles/#{profile_id}")
|
|
592
613
|
profile = response[:data]
|
|
593
614
|
|
|
594
|
-
# Determine output path
|
|
615
|
+
# Determine output path. Default to ~/Downloads/ (created if
|
|
616
|
+
# missing) instead of the current working directory — users
|
|
617
|
+
# were ending up with .mobileprovision files sprinkled inside
|
|
618
|
+
# whatever project they ran the command from.
|
|
595
619
|
output_path = if options[:output]
|
|
596
620
|
options[:output]
|
|
597
621
|
else
|
|
598
|
-
# Use profile name, sanitize it for filename
|
|
599
622
|
name = profile['name'] || "profile_#{profile['id']}"
|
|
600
623
|
filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
|
|
601
|
-
|
|
624
|
+
downloads_dir = File.expand_path('~/Downloads')
|
|
625
|
+
FileUtils.mkdir_p(downloads_dir)
|
|
626
|
+
File.join(downloads_dir, "#{filename}.mobileprovision")
|
|
602
627
|
end
|
|
603
628
|
|
|
604
629
|
# Download the profile content using the client's connection with auth
|
|
@@ -924,14 +949,17 @@ module Mysigner
|
|
|
924
949
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/certificates/#{certificate_id}")
|
|
925
950
|
certificate = response[:data]
|
|
926
951
|
|
|
927
|
-
# Determine output path
|
|
952
|
+
# Determine output path. Default to ~/Downloads/ (mirrors
|
|
953
|
+
# profile download behavior) rather than the CWD to avoid
|
|
954
|
+
# dropping .cer files inside the user's project tree.
|
|
928
955
|
output_path = if options[:output]
|
|
929
956
|
options[:output]
|
|
930
957
|
else
|
|
931
|
-
# Use certificate name, sanitize it for filename
|
|
932
958
|
name = certificate['name'] || "certificate_#{certificate['id']}"
|
|
933
959
|
filename = name.gsub(/[^0-9A-Za-z.-]/, '_')
|
|
934
|
-
|
|
960
|
+
downloads_dir = File.expand_path('~/Downloads')
|
|
961
|
+
FileUtils.mkdir_p(downloads_dir)
|
|
962
|
+
File.join(downloads_dir, "#{filename}.cer")
|
|
935
963
|
end
|
|
936
964
|
|
|
937
965
|
# Download the certificate content (binary response)
|
|
@@ -1127,14 +1155,33 @@ module Mysigner
|
|
|
1127
1155
|
say '🔐 Uploading keystore...', :cyan
|
|
1128
1156
|
say ''
|
|
1129
1157
|
|
|
1130
|
-
#
|
|
1158
|
+
# Phase 0: support non-TTY automation. `ask(echo: false)` raises
|
|
1159
|
+
# Errno::ENOTTY on piped stdin, which made `keystore upload`
|
|
1160
|
+
# unusable from CI. When stdin isn't a TTY, require passwords
|
|
1161
|
+
# to come from env vars (MYSIGNER_KEYSTORE_PASSWORD /
|
|
1162
|
+
# MYSIGNER_KEY_PASSWORD) so operators can script uploads
|
|
1163
|
+
# without also having to wrap every invocation in `expect`.
|
|
1131
1164
|
name = options[:name] || ask("Keystore name (e.g., 'Release Key'):")
|
|
1132
1165
|
key_alias = options[:alias] || ask('Key alias:')
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1166
|
+
|
|
1167
|
+
if $stdin.tty?
|
|
1168
|
+
password = ask('Keystore password:', echo: false)
|
|
1169
|
+
say ''
|
|
1170
|
+
key_password = ask('Key password (press Enter if same as keystore):', echo: false)
|
|
1171
|
+
say ''
|
|
1172
|
+
key_password = password if key_password.empty?
|
|
1173
|
+
else
|
|
1174
|
+
password = ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
|
|
1175
|
+
key_password = ENV['MYSIGNER_KEY_PASSWORD'] || password
|
|
1176
|
+
if password.nil? || password.empty?
|
|
1177
|
+
error 'Non-interactive upload requires MYSIGNER_KEYSTORE_PASSWORD (and optionally MYSIGNER_KEY_PASSWORD) in the environment.'
|
|
1178
|
+
say ''
|
|
1179
|
+
say 'Example:', :cyan
|
|
1180
|
+
say ' MYSIGNER_KEYSTORE_PASSWORD=... MYSIGNER_KEY_PASSWORD=... \\', :yellow
|
|
1181
|
+
say ' mysigner keystore upload ./release.jks --name "Release" --alias myalias', :yellow
|
|
1182
|
+
exit 1
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1138
1185
|
|
|
1139
1186
|
begin
|
|
1140
1187
|
result = manager.upload(
|
|
@@ -1742,35 +1789,64 @@ module Mysigner
|
|
|
1742
1789
|
exit 1
|
|
1743
1790
|
end
|
|
1744
1791
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1792
|
+
# Phase 0: do NOT write plaintext key.properties into the user's
|
|
1793
|
+
# project tree. Use a two-step build so we can inject signing via
|
|
1794
|
+
# a Gradle init script on the Gradle step only. Env vars are set
|
|
1795
|
+
# on the child process; passwords never appear in argv or on disk.
|
|
1796
|
+
require_relative '../signing/gradle_signing_injector'
|
|
1747
1797
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1798
|
+
injector = nil
|
|
1799
|
+
env = {}
|
|
1800
|
+
init_path = nil
|
|
1750
1801
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
end
|
|
1802
|
+
if keystore_info
|
|
1803
|
+
injector = Mysigner::Signing::GradleSigningInjector.new
|
|
1804
|
+
init_path = injector.write_init_script!
|
|
1805
|
+
env = keystore_info[:signing_env_vars] || injector.env_vars(
|
|
1806
|
+
keystore_path: keystore_info[:path],
|
|
1807
|
+
store_password: keystore_info[:password],
|
|
1808
|
+
key_password: keystore_info[:key_password],
|
|
1809
|
+
key_alias: keystore_info[:key_alias]
|
|
1810
|
+
)
|
|
1811
|
+
end
|
|
1762
1812
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1813
|
+
begin
|
|
1814
|
+
Dir.chdir(project_dir) do
|
|
1815
|
+
if keystore_info
|
|
1816
|
+
# Step 1: Flutter prepares the Gradle project (no signing needed).
|
|
1817
|
+
prepare_args = ['flutter', 'build', 'appbundle', '--release', '--config-only']
|
|
1818
|
+
prepare_args += ['--build-number', version_code_override.to_s] if version_code_override
|
|
1819
|
+
unless system(env, *prepare_args)
|
|
1820
|
+
error 'Flutter prebuild (--config-only) failed'
|
|
1821
|
+
exit 1
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
# Step 2: invoke Gradle directly so we can pass --init-script.
|
|
1825
|
+
Dir.chdir(File.join(project_dir, 'android')) do
|
|
1826
|
+
gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all', '--init-script', init_path]
|
|
1827
|
+
gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
|
|
1828
|
+
unless system(env, *gradle_args)
|
|
1829
|
+
error 'Gradle bundleRelease failed'
|
|
1830
|
+
exit 1
|
|
1831
|
+
end
|
|
1832
|
+
end
|
|
1833
|
+
else
|
|
1834
|
+
# No keystore: plain Flutter build (debug signing).
|
|
1835
|
+
args = ['flutter', 'build', 'appbundle', '--release']
|
|
1836
|
+
args += ['--build-number', version_code_override.to_s] if version_code_override
|
|
1837
|
+
unless system(*args)
|
|
1838
|
+
error 'Flutter build failed'
|
|
1839
|
+
exit 1
|
|
1840
|
+
end
|
|
1841
|
+
end
|
|
1767
1842
|
end
|
|
1843
|
+
ensure
|
|
1844
|
+
injector&.cleanup!
|
|
1768
1845
|
end
|
|
1769
1846
|
|
|
1770
1847
|
# Flutter outputs to build/app/outputs/bundle/release/
|
|
1771
1848
|
aab_path = File.join(project_dir, 'build/app/outputs/bundle/release/app-release.aab')
|
|
1772
1849
|
unless File.exist?(aab_path)
|
|
1773
|
-
# Try alternate paths
|
|
1774
1850
|
alt_paths = Dir.glob(File.join(project_dir, 'build/app/outputs/bundle/*/*.aab'))
|
|
1775
1851
|
aab_path = alt_paths.first if alt_paths.any?
|
|
1776
1852
|
end
|
|
@@ -1858,47 +1934,45 @@ module Mysigner
|
|
|
1858
1934
|
return nil unless config.exists?
|
|
1859
1935
|
|
|
1860
1936
|
config.load
|
|
1861
|
-
return nil unless config.api_token && config.
|
|
1937
|
+
return nil unless config.api_token && config.current_organization_id
|
|
1862
1938
|
|
|
1863
1939
|
client = Mysigner::Client.new(api_url: config.api_url, api_token: config.api_token,
|
|
1864
1940
|
user_email: config.user_email)
|
|
1865
|
-
keystore_manager = Signing::KeystoreManager.new(client, config.
|
|
1941
|
+
keystore_manager = Signing::KeystoreManager.new(client, config.current_organization_id)
|
|
1866
1942
|
|
|
1867
1943
|
# Find app by package name to get its keystore
|
|
1868
|
-
response = client.get("/api/v1/organizations/#{config.
|
|
1944
|
+
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
|
|
1869
1945
|
apps = response[:data]['android_apps'] || []
|
|
1870
1946
|
app = apps.find { |a| a['package_name'] == package_name }
|
|
1871
1947
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1948
|
+
keystore = nil
|
|
1949
|
+
keystore = keystore_manager.active_keystore(android_app_id: app['id']) if app
|
|
1950
|
+
keystore ||= keystore_manager.active_keystore
|
|
1951
|
+
return nil unless keystore
|
|
1952
|
+
|
|
1953
|
+
# Phase 0: fetch passwords via narrow audit-logged /secrets endpoint
|
|
1954
|
+
# instead of the deprecated ?include_secrets=true param on the list.
|
|
1955
|
+
secrets = keystore_manager.fetch_secrets(keystore['id'])
|
|
1956
|
+
downloaded = keystore_manager.get_or_download(keystore['id'])
|
|
1957
|
+
password = secrets['keystore_password']
|
|
1958
|
+
key_password = secrets['key_password'] || password
|
|
1959
|
+
key_alias = secrets['key_alias'] || keystore['key_alias']
|
|
1960
|
+
{
|
|
1961
|
+
path: downloaded[:path],
|
|
1962
|
+
name: keystore['name'],
|
|
1963
|
+
password: password,
|
|
1964
|
+
key_alias: key_alias,
|
|
1965
|
+
key_password: key_password,
|
|
1966
|
+
id: keystore['id'],
|
|
1967
|
+
# Ready-to-spawn env vars consumed by GradleSigningInjector in
|
|
1968
|
+
# build_gradle_aab / build_flutter_aab / Build::AndroidExecutor.
|
|
1969
|
+
signing_env_vars: {
|
|
1970
|
+
'MYSIGNER_STORE_FILE' => downloaded[:path],
|
|
1971
|
+
'MYSIGNER_STORE_PASSWORD' => password,
|
|
1972
|
+
'MYSIGNER_KEY_PASSWORD' => key_password,
|
|
1973
|
+
'MYSIGNER_KEY_ALIAS' => key_alias
|
|
1898
1974
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
nil
|
|
1975
|
+
}
|
|
1902
1976
|
rescue StandardError
|
|
1903
1977
|
# Silently fail - we'll use debug signing
|
|
1904
1978
|
nil
|
|
@@ -1912,45 +1986,84 @@ module Mysigner
|
|
|
1912
1986
|
exit 1
|
|
1913
1987
|
end
|
|
1914
1988
|
|
|
1989
|
+
require 'tmpdir'
|
|
1990
|
+
pw_tmpdir = nil
|
|
1991
|
+
store_pw_path = nil
|
|
1992
|
+
key_pw_path = nil
|
|
1993
|
+
|
|
1915
1994
|
Dir.chdir(project_dir) do
|
|
1916
1995
|
base_args = []
|
|
1917
1996
|
|
|
1918
|
-
#
|
|
1997
|
+
# Phase 0: For AAB output, MSBuild only supports `file:<path>`
|
|
1998
|
+
# for AndroidSigning*Pass (env: is explicitly unsupported for AAB).
|
|
1999
|
+
# Write passwords to 0600 tempfiles whose *paths* go into argv
|
|
2000
|
+
# instead of the passwords themselves.
|
|
1919
2001
|
if keystore_info
|
|
2002
|
+
pw_tmpdir = Dir.mktmpdir('mysigner-maui-pw-')
|
|
2003
|
+
store_pw_path = File.join(pw_tmpdir, 'store_pw.txt')
|
|
2004
|
+
File.write(store_pw_path, keystore_info[:password].to_s)
|
|
2005
|
+
File.chmod(0o600, store_pw_path)
|
|
2006
|
+
|
|
2007
|
+
# Reuse the same file if store/key passwords match (common)
|
|
2008
|
+
if keystore_info[:key_password] == keystore_info[:password]
|
|
2009
|
+
key_pw_path = store_pw_path
|
|
2010
|
+
else
|
|
2011
|
+
key_pw_path = File.join(pw_tmpdir, 'key_pw.txt')
|
|
2012
|
+
File.write(key_pw_path, keystore_info[:key_password].to_s)
|
|
2013
|
+
File.chmod(0o600, key_pw_path)
|
|
2014
|
+
end
|
|
2015
|
+
|
|
1920
2016
|
base_args += [
|
|
1921
2017
|
'-p:AndroidKeyStore=true',
|
|
1922
2018
|
"-p:AndroidSigningKeyStore=#{keystore_info[:path]}",
|
|
1923
2019
|
"-p:AndroidSigningKeyAlias=#{keystore_info[:key_alias]}",
|
|
1924
|
-
"-p:AndroidSigningKeyPass
|
|
1925
|
-
"-p:AndroidSigningStorePass
|
|
2020
|
+
"-p:AndroidSigningKeyPass=file:#{key_pw_path}",
|
|
2021
|
+
"-p:AndroidSigningStorePass=file:#{store_pw_path}"
|
|
1926
2022
|
]
|
|
1927
2023
|
end
|
|
1928
2024
|
|
|
1929
2025
|
# Add version code override
|
|
1930
2026
|
base_args << "-p:ApplicationVersion=#{version_code_override}" if version_code_override
|
|
1931
2027
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2028
|
+
begin
|
|
2029
|
+
# MAUI uses dotnet publish with Android target
|
|
2030
|
+
success = if framework == :maui
|
|
2031
|
+
system(
|
|
2032
|
+
'dotnet', 'publish',
|
|
2033
|
+
'-f', 'net8.0-android',
|
|
2034
|
+
'-c', 'Release',
|
|
2035
|
+
'-p:AndroidPackageFormat=aab',
|
|
2036
|
+
*base_args
|
|
2037
|
+
)
|
|
2038
|
+
else
|
|
2039
|
+
# Xamarin uses msbuild
|
|
2040
|
+
system(
|
|
2041
|
+
'dotnet', 'build',
|
|
2042
|
+
'-c', 'Release',
|
|
2043
|
+
'-p:AndroidPackageFormat=aab',
|
|
2044
|
+
*base_args
|
|
2045
|
+
)
|
|
2046
|
+
end
|
|
2047
|
+
|
|
2048
|
+
unless success
|
|
2049
|
+
error '.NET build failed'
|
|
2050
|
+
exit 1
|
|
2051
|
+
end
|
|
2052
|
+
ensure
|
|
2053
|
+
# Clean up password files. On Windows NTFS the child may still
|
|
2054
|
+
# hold the file open; fall back to at_exit deferred delete.
|
|
2055
|
+
[store_pw_path, key_pw_path].compact.uniq.each do |p|
|
|
2056
|
+
File.delete(p) if File.exist?(p)
|
|
2057
|
+
rescue Errno::EACCES
|
|
2058
|
+
at_exit do
|
|
2059
|
+
File.delete(p)
|
|
2060
|
+
rescue StandardError
|
|
2061
|
+
nil
|
|
2062
|
+
end
|
|
2063
|
+
rescue StandardError
|
|
2064
|
+
# Best effort
|
|
2065
|
+
end
|
|
2066
|
+
FileUtils.rm_rf(pw_tmpdir) if pw_tmpdir && Dir.exist?(pw_tmpdir)
|
|
1954
2067
|
end
|
|
1955
2068
|
end
|
|
1956
2069
|
|
|
@@ -1976,28 +2089,38 @@ module Mysigner
|
|
|
1976
2089
|
exit 1
|
|
1977
2090
|
end
|
|
1978
2091
|
|
|
1979
|
-
#
|
|
2092
|
+
# Phase 0: inject signing via Gradle init-script + env vars so
|
|
2093
|
+
# passwords never appear in `ps aux` (ORG_GRADLE_PROJECT_<name>
|
|
2094
|
+
# was the old workaround but doesn't support dotted names).
|
|
2095
|
+
require_relative '../signing/gradle_signing_injector'
|
|
2096
|
+
|
|
1980
2097
|
gradle_args = ['./gradlew', 'bundleRelease', '--warning-mode=all']
|
|
2098
|
+
gradle_args << "-PversionCode=#{version_code_override}" if version_code_override
|
|
1981
2099
|
|
|
2100
|
+
injector = nil
|
|
2101
|
+
env = {}
|
|
1982
2102
|
if keystore_info
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2103
|
+
injector = Mysigner::Signing::GradleSigningInjector.new
|
|
2104
|
+
init_path = injector.write_init_script!
|
|
2105
|
+
env = keystore_info[:signing_env_vars] || injector.env_vars(
|
|
2106
|
+
keystore_path: keystore_info[:path],
|
|
2107
|
+
store_password: keystore_info[:password],
|
|
2108
|
+
key_password: keystore_info[:key_password],
|
|
2109
|
+
key_alias: keystore_info[:key_alias]
|
|
2110
|
+
)
|
|
2111
|
+
gradle_args.insert(1, '--init-script', init_path)
|
|
1990
2112
|
end
|
|
1991
2113
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
exit 1
|
|
2114
|
+
begin
|
|
2115
|
+
Dir.chdir(android_dir) do
|
|
2116
|
+
success = system(env, *gradle_args)
|
|
2117
|
+
unless success
|
|
2118
|
+
error 'Gradle build failed'
|
|
2119
|
+
exit 1
|
|
2120
|
+
end
|
|
2000
2121
|
end
|
|
2122
|
+
ensure
|
|
2123
|
+
injector&.cleanup!
|
|
2001
2124
|
end
|
|
2002
2125
|
|
|
2003
2126
|
# Find the AAB
|
|
@@ -2099,19 +2222,19 @@ module Mysigner
|
|
|
2099
2222
|
|
|
2100
2223
|
case action
|
|
2101
2224
|
when 'register'
|
|
2102
|
-
if args.empty?
|
|
2225
|
+
if args.empty? || args[0].nil? || args[0].to_s.empty?
|
|
2103
2226
|
error 'Usage: mysigner bundleid register IDENTIFIER [NAME]'
|
|
2104
2227
|
say ''
|
|
2105
2228
|
say 'Example: mysigner bundleid register com.company.myapp', :yellow
|
|
2106
2229
|
say 'Example: mysigner bundleid register com.company.myapp.widget "My Widget"', :yellow
|
|
2107
2230
|
exit 1
|
|
2231
|
+
return
|
|
2108
2232
|
end
|
|
2109
2233
|
|
|
2110
2234
|
identifier = args[0]
|
|
2111
|
-
# Default name is the last component of the identifier
|
|
2112
|
-
name = args[1] || identifier.split('.').last.capitalize
|
|
2113
2235
|
|
|
2114
|
-
# Validate bundle ID format
|
|
2236
|
+
# Validate bundle ID format BEFORE dereferencing (guards against
|
|
2237
|
+
# `.split` on a degenerate identifier like "123.com.app").
|
|
2115
2238
|
unless identifier =~ /^[a-zA-Z][a-zA-Z0-9.-]*\.[a-zA-Z][a-zA-Z0-9.-]*$/
|
|
2116
2239
|
error "Invalid Bundle ID format: #{identifier}"
|
|
2117
2240
|
say ''
|
|
@@ -2120,8 +2243,12 @@ module Mysigner
|
|
|
2120
2243
|
say ' • Use reverse domain notation (e.g., com.company.app)', :cyan
|
|
2121
2244
|
say ' • Contain only letters, numbers, hyphens, and periods', :cyan
|
|
2122
2245
|
exit 1
|
|
2246
|
+
return
|
|
2123
2247
|
end
|
|
2124
2248
|
|
|
2249
|
+
# Default name is the last component of the identifier
|
|
2250
|
+
name = args[1] || identifier.split('.').last.capitalize
|
|
2251
|
+
|
|
2125
2252
|
say '🔗 Registering Bundle ID...', :cyan
|
|
2126
2253
|
say ''
|
|
2127
2254
|
say " Identifier: #{identifier}", :white
|
|
@@ -2205,12 +2332,67 @@ module Mysigner
|
|
|
2205
2332
|
exit 1
|
|
2206
2333
|
end
|
|
2207
2334
|
|
|
2335
|
+
when 'delete'
|
|
2336
|
+
if args.empty? || args[0].nil? || args[0].to_s.empty?
|
|
2337
|
+
error 'Usage: mysigner bundleid delete IDENTIFIER'
|
|
2338
|
+
say ''
|
|
2339
|
+
say 'Example: mysigner bundleid delete com.company.myapp', :yellow
|
|
2340
|
+
exit 1
|
|
2341
|
+
return
|
|
2342
|
+
end
|
|
2343
|
+
|
|
2344
|
+
identifier = args[0]
|
|
2345
|
+
|
|
2346
|
+
say '🗑 Removing Bundle ID...', :cyan
|
|
2347
|
+
say " Identifier: #{identifier}", :white
|
|
2348
|
+
say ''
|
|
2349
|
+
|
|
2350
|
+
begin
|
|
2351
|
+
# Resolve the identifier → numeric id (the backend DELETE is keyed on id).
|
|
2352
|
+
list_response = client.get(
|
|
2353
|
+
"/api/v1/organizations/#{config.current_organization_id}/bundle_ids"
|
|
2354
|
+
)
|
|
2355
|
+
bundle_ids = list_response[:data]['bundle_ids'] || list_response[:data] || []
|
|
2356
|
+
bid = bundle_ids.find { |b| (b['identifier'] || b['bundle_id']) == identifier }
|
|
2357
|
+
|
|
2358
|
+
if bid.nil?
|
|
2359
|
+
error "Bundle ID not found: #{identifier}"
|
|
2360
|
+
say ''
|
|
2361
|
+
say ' → List existing: mysigner bundleid list', :yellow
|
|
2362
|
+
exit 1
|
|
2363
|
+
return
|
|
2364
|
+
end
|
|
2365
|
+
|
|
2366
|
+
client.delete(
|
|
2367
|
+
"/api/v1/organizations/#{config.current_organization_id}/bundle_ids/#{bid['id']}"
|
|
2368
|
+
)
|
|
2369
|
+
|
|
2370
|
+
say '✓ Bundle ID deleted from App Store Connect and local cache', :green
|
|
2371
|
+
say ''
|
|
2372
|
+
say 'Run `mysigner sync ios` to refresh your local cache if needed.', :cyan
|
|
2373
|
+
rescue Mysigner::ValidationError => e
|
|
2374
|
+
# Backend maps 409 Conflict → ValidationError. Apple refused
|
|
2375
|
+
# because of dependent resources (apps/profiles/capabilities).
|
|
2376
|
+
error e.message
|
|
2377
|
+
say ''
|
|
2378
|
+
say '💡 To delete this Bundle ID:', :cyan
|
|
2379
|
+
say ' 1. Remove any provisioning profiles that use it', :yellow
|
|
2380
|
+
say ' 2. Remove any apps tied to it in App Store Connect', :yellow
|
|
2381
|
+
say ' 3. Disable capabilities attached to it', :yellow
|
|
2382
|
+
say ' 4. Re-run `mysigner bundleid delete`', :yellow
|
|
2383
|
+
exit 1
|
|
2384
|
+
rescue Mysigner::ClientError => e
|
|
2385
|
+
error "Failed to delete Bundle ID: #{e.message}"
|
|
2386
|
+
exit 1
|
|
2387
|
+
end
|
|
2388
|
+
|
|
2208
2389
|
else
|
|
2209
2390
|
error "Unknown action: #{action}"
|
|
2210
2391
|
say ''
|
|
2211
2392
|
say 'Available actions:', :yellow
|
|
2212
2393
|
say ' mysigner bundleid register IDENTIFIER [NAME]', :cyan
|
|
2213
2394
|
say ' mysigner bundleid list', :cyan
|
|
2395
|
+
say ' mysigner bundleid delete IDENTIFIER', :cyan
|
|
2214
2396
|
exit 1
|
|
2215
2397
|
end
|
|
2216
2398
|
end
|
|
@@ -2407,11 +2589,12 @@ module Mysigner
|
|
|
2407
2589
|
|
|
2408
2590
|
case action
|
|
2409
2591
|
when 'create'
|
|
2410
|
-
if identifier.nil?
|
|
2592
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2411
2593
|
error 'Usage: mysigner merchant-id create IDENTIFIER [--name NAME]'
|
|
2412
2594
|
say ''
|
|
2413
2595
|
say 'Example: mysigner merchant-id create merchant.com.company.app', :yellow
|
|
2414
2596
|
exit 1
|
|
2597
|
+
return
|
|
2415
2598
|
end
|
|
2416
2599
|
|
|
2417
2600
|
unless identifier.start_with?('merchant.')
|
|
@@ -2419,6 +2602,7 @@ module Mysigner
|
|
|
2419
2602
|
say ''
|
|
2420
2603
|
say 'Example: merchant.com.company.app', :cyan
|
|
2421
2604
|
exit 1
|
|
2605
|
+
return
|
|
2422
2606
|
end
|
|
2423
2607
|
|
|
2424
2608
|
say '💳 Creating Merchant ID...', :cyan
|
|
@@ -2448,9 +2632,10 @@ module Mysigner
|
|
|
2448
2632
|
end
|
|
2449
2633
|
|
|
2450
2634
|
when 'delete'
|
|
2451
|
-
if identifier.nil?
|
|
2635
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2452
2636
|
error 'Usage: mysigner merchant-id delete IDENTIFIER'
|
|
2453
2637
|
exit 1
|
|
2638
|
+
return
|
|
2454
2639
|
end
|
|
2455
2640
|
|
|
2456
2641
|
say '💳 Deleting Merchant ID...', :cyan
|
|
@@ -2468,6 +2653,7 @@ module Mysigner
|
|
|
2468
2653
|
if m.nil?
|
|
2469
2654
|
error "Merchant ID not found: #{identifier}"
|
|
2470
2655
|
exit 1
|
|
2656
|
+
return
|
|
2471
2657
|
end
|
|
2472
2658
|
|
|
2473
2659
|
client.delete(
|
|
@@ -2739,13 +2925,14 @@ module Mysigner
|
|
|
2739
2925
|
|
|
2740
2926
|
case action
|
|
2741
2927
|
when 'register'
|
|
2742
|
-
if identifier.nil?
|
|
2928
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2743
2929
|
error 'Usage: mysigner app-group register IDENTIFIER [--name NAME]'
|
|
2744
2930
|
say ''
|
|
2745
2931
|
say 'Example: mysigner app-group register group.com.company.shared', :yellow
|
|
2746
2932
|
say ''
|
|
2747
2933
|
say 'Note: Create the App Group in Apple Developer Portal first!', :cyan
|
|
2748
2934
|
exit 1
|
|
2935
|
+
return
|
|
2749
2936
|
end
|
|
2750
2937
|
|
|
2751
2938
|
unless identifier.start_with?('group.')
|
|
@@ -2753,6 +2940,7 @@ module Mysigner
|
|
|
2753
2940
|
say ''
|
|
2754
2941
|
say 'Example: group.com.company.shared', :cyan
|
|
2755
2942
|
exit 1
|
|
2943
|
+
return
|
|
2756
2944
|
end
|
|
2757
2945
|
|
|
2758
2946
|
say '📦 Registering App Group...', :cyan
|
|
@@ -2785,9 +2973,10 @@ module Mysigner
|
|
|
2785
2973
|
end
|
|
2786
2974
|
|
|
2787
2975
|
when 'delete'
|
|
2788
|
-
if identifier.nil?
|
|
2976
|
+
if identifier.nil? || identifier.to_s.empty?
|
|
2789
2977
|
error 'Usage: mysigner app-group delete IDENTIFIER'
|
|
2790
2978
|
exit 1
|
|
2979
|
+
return
|
|
2791
2980
|
end
|
|
2792
2981
|
|
|
2793
2982
|
say '📦 Removing App Group...', :cyan
|
|
@@ -2805,6 +2994,7 @@ module Mysigner
|
|
|
2805
2994
|
if g.nil?
|
|
2806
2995
|
error "App Group not found: #{identifier}"
|
|
2807
2996
|
exit 1
|
|
2997
|
+
return
|
|
2808
2998
|
end
|
|
2809
2999
|
|
|
2810
3000
|
client.delete(
|
data/lib/mysigner/cli.rb
CHANGED
|
@@ -14,6 +14,14 @@ require_relative 'cli/diagnostic_commands'
|
|
|
14
14
|
require_relative 'cli/build_commands'
|
|
15
15
|
require_relative 'cli/resource_commands'
|
|
16
16
|
require_relative 'cli/validate_commands'
|
|
17
|
+
require_relative 'cleanup/private_keys_purger'
|
|
18
|
+
|
|
19
|
+
# Phase 0: one-time cleanup of legacy plaintext .p8 files that older CLI
|
|
20
|
+
# versions wrote to ~/.private_keys/ and ~/.appstoreconnect/private_keys/.
|
|
21
|
+
# Idempotent — a marker file at ~/.mysigner/.private_keys_purged prevents
|
|
22
|
+
# re-running. Skipped when MYSIGNER_USE_LEGACY_ASC=1 so users who opted
|
|
23
|
+
# back into the legacy altool path keep their existing keys.
|
|
24
|
+
Mysigner::Cleanup::PrivateKeysPurger.new.call
|
|
17
25
|
|
|
18
26
|
module Mysigner
|
|
19
27
|
class CLI < Thor
|