Module: Mysigner::CLI::Concerns::Helpers
- Included in:
- Mysigner::CLI
- Defined in:
- lib/mysigner/cli/concerns/helpers.rb
Class Method Summary collapse
Instance Method Summary collapse
-
#blank_local_only_config ⇒ Object
mysigner-22 — a Config built without touching disk, ENV, or the encryption key.
- #create_client(config) ⇒ Object
-
#emit_local_only_banner ⇒ Object
One-time banner on stderr (TTY only) so users know they’ve opted into local-only mode.
- #error(message) ⇒ Object
-
#exit_unless_local_supported!(command_name) ⇒ Object
Server-only command guard.
- #format_bytes(bytes) ⇒ Object
- #format_duration(seconds) ⇒ Object
- #load_config ⇒ Object
-
#local_only? ⇒ Boolean
Local-only mode is active when any of, in precedence order: 1.
-
#resolve_local_android_keystore_or_exit ⇒ Object
mysigner-22 Phase 7 — Android keystore counterpart of the ASC/Play resolvers.
-
#resolve_local_asc_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — resolve ASC creds via the cascade (flag → env → keychain → ~/.appstoreconnect → prompt), surface a clear error and exit 1 on miss.
-
#resolve_local_play_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — Google Play counterpart of resolve_local_asc_creds_or_exit.
-
#valid_ios_udid?(udid) ⇒ Boolean
Client-side UDID validity check for iOS devices.
-
#with_timing(_label) ⇒ Object
Helper for timing operations.
Class Method Details
.banner_emitted? ⇒ Boolean
250 251 252 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 250 def @local_only_banner_emitted == true end |
.mark_banner_emitted! ⇒ Object
254 255 256 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 254 def @local_only_banner_emitted = true end |
.reset_banner! ⇒ Object
258 259 260 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 258 def @local_only_banner_emitted = false end |
Instance Method Details
#blank_local_only_config ⇒ Object
mysigner-22 — a Config built without touching disk, ENV, or the encryption key. We deliberately bypass Config#initialize (which auto-loads ~/.mysigner/config.yml when it exists) because the whole point of local-only is to work on a machine where that file might not exist or might be unreadable (e.g. broken Keychain key). Callers only read ‘current_organization_id` / `api_url` / etc., all of which legitimately return nil here.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 134 def blank_local_only_config config = Config.allocate config.instance_variable_set(:@api_url, nil) config.instance_variable_set(:@user_email, nil) config.instance_variable_set(:@current_organization_id, nil) config.instance_variable_set(:@organizations, {}) config.instance_variable_set(:@encryption_enabled, false) config.instance_variable_set(:@from_env, false) # The whole point of this sentinel is to BE a local-only config — # set @local_only = true so `config.local_only?` (and any caller # reading `config.local_only`) agrees. config.instance_variable_set(:@local_only, true) config end |
#create_client(config) ⇒ Object
113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 113 def create_client(config) # mysigner-22 — in local-only mode every MySigner API touchpoint is # supposed to be bypassed at the call site. Returning nil here makes # an accidental `client.get(...)` fail loud (NoMethodError on nil) # rather than silently re-introducing a server hit. return nil if local_only? Client.new( api_url: config.api_url, api_token: config.api_token, user_email: config.user_email ) end |
#emit_local_only_banner ⇒ Object
One-time banner on stderr (TTY only) so users know they’ve opted into local-only mode. Module-level guard ensures it fires at most once per CLI invocation, even if multiple commands call it. (Module instance var, not @@, to avoid the class-var smell — every instance method sees the same Helpers module object.)
220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 220 def return if Helpers. return unless $stderr.respond_to?(:tty?) && $stderr.tty? Helpers. # Honest scope: credential transport is what local-only currently # guards. Non-credential MySigner endpoints (app/build registry, # keystore download, etc.) are still used. Documented in the # local-only docs (mysigner-45). warn '[mysigner] local-only mode active — signing credentials stay on this machine ' \ '(other MySigner APIs may still be used; see docs).' end |
#error(message) ⇒ Object
149 150 151 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 149 def error() say "✗ Error: #{}", :red end |
#exit_unless_local_supported!(command_name) ⇒ Object
Server-only command guard. SERVER commands (apps, orgs, sync, certificates, etc.) hit MySigner-side resources and have no local equivalent. Print a clean explanation and exit 2 when local-only mode is active, instead of letting load_config bail with the generic “Not logged in” path.
238 239 240 241 242 243 244 245 246 247 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 238 def exit_unless_local_supported!(command_name) return unless local_only? say "✗ `#{command_name}` manages MySigner-side resources and " \ "isn't available in local-only mode.", :red say '' say 'Disable persistently: mysigner config set local-only false', :yellow say "Override for one call: mysigner --no-local-only #{command_name}", :yellow exit 2 end |
#format_bytes(bytes) ⇒ Object
29 30 31 32 33 34 35 36 37 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 29 def format_bytes(bytes) if bytes < 1024 "#{bytes} B" elsif bytes < 1024 * 1024 "#{(bytes / 1024.0).round(1)} KB" else "#{(bytes / (1024.0 * 1024)).round(1)} MB" end end |
#format_duration(seconds) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 15 def format_duration(seconds) if seconds < 60 "#{seconds.round}s" elsif seconds < 3600 minutes = (seconds / 60).floor secs = (seconds % 60).round "#{minutes}m #{secs}s" else hours = (seconds / 3600).floor minutes = ((seconds % 3600) / 60).floor "#{hours}h #{minutes}m" end end |
#load_config ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 71 def load_config # mysigner-22 — local-only mode is allowed to run with ZERO MySigner # auth: no ~/.mysigner/config.yml, no API token, no login. Return a # blank Config sentinel and skip both the load and the # "Not logged in" exit. Callers must treat the returned config as # opaque (no api_url, no api_token, no organization_id) and must # also treat create_client(config) as returning nil. return blank_local_only_config if local_only? # CI/CD mode: prefer environment variables when set return Config.from_env if Config.env_configured? config = Config.new unless config.exists? error "Not logged in. Run 'mysigner login' first." say '' say 'Tip: For CI/CD, set these environment variables instead:', :yellow say ' export MYSIGNER_API_TOKEN=your_token', :yellow say ' export MYSIGNER_ORG_ID=your_org_id', :yellow say ' export MYSIGNER_API_URL=https://mysigner.dev # optional', :yellow say ' export MYSIGNER_EMAIL=you@example.com # optional', :yellow say '' # Discoverability: a first-time user who doesn't want a MySigner # account at all (BYO-credentials, no server orchestration) needs # to know `--local-only` exists. Without this tip the only error # they see is "Not logged in", which strongly implies signup is # the only path forward. say 'Tip: To skip MySigner entirely and ship locally with your own', :yellow say 'Apple/Google credentials, use --local-only:', :yellow say ' mysigner --local-only ship appstore', :yellow say ' (auto-discovers ASC .p8 from ~/.appstoreconnect/private_keys/,', :yellow say ' Google Play SA-JSON from GOOGLE_APPLICATION_CREDENTIALS / eas.json,', :yellow say ' keystore from key.properties / eas.json — or set them via flags / env.)', :yellow say ' See "Local-only mode" section in README.', :yellow exit 1 end config.load config end |
#local_only? ⇒ Boolean
Local-only mode is active when any of, in precedence order:
1. --local-only / --no-local-only flag on this invocation
2. MYSIGNER_LOCAL_ONLY env var
3. `local_only: true` in ~/.mysigner/config.yml
‘Config.local_only?` (class method) walks #2 then #3.
158 159 160 161 162 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 158 def local_only? return [:local_only] unless [:local_only].nil? Mysigner::Config.local_only? end |
#resolve_local_android_keystore_or_exit ⇒ Object
mysigner-22 Phase 7 — Android keystore counterpart of the ASC/Play resolvers. Pre-resolves the keystore (path + passwords + alias) via the cascade so ‘ship android –local-only` can skip the MySigner server entirely.
204 205 206 207 208 209 210 211 212 213 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 204 def resolve_local_android_keystore_or_exit require 'mysigner/credential_resolver' creds = Mysigner::CredentialResolver.resolve_android_keystore(options: .to_h) warn "[mysigner] Android keystore source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty? creds rescue Mysigner::CredentialResolver::CredentialNotFoundError, Mysigner::CredentialResolver::AmbiguousCredentialsError => e say "✗ No local Android keystore found:\n#{e.}", :red exit 1 end |
#resolve_local_asc_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — resolve ASC creds via the cascade (flag →env → keychain → ~/.appstoreconnect → prompt), surface a clear error and exit 1 on miss. Logs the winning source on stderr so CI runs leave an audit trail of where the credential came from. Returns a Mysigner::CredentialResolver::AscCreds struct.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 169 def resolve_local_asc_creds_or_exit require 'mysigner/credential_resolver' creds = Mysigner::CredentialResolver.resolve_asc(options: .to_h) warn "[mysigner] ASC credentials source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty? creds rescue Mysigner::CredentialResolver::CredentialNotFoundError, Mysigner::CredentialResolver::AmbiguousCredentialsError => e # Preserve the historical wording the CLI specs were written # against ("No local ASC credentials found") so users and tests # have a stable identifier, while including the resolver's # multi-line cascade trace + override knob list right after it. say "✗ No local ASC credentials found via `mysigner onboard --local-only` or other sources:\n#{e.}", :red exit 1 end |
#resolve_local_play_creds_or_exit ⇒ Object
mysigner-22 Phase 5 — Google Play counterpart of resolve_local_asc_creds_or_exit. Same semantics: pre-resolve so the CLI can fail fast before the build kicks off.
188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 188 def resolve_local_play_creds_or_exit require 'mysigner/credential_resolver' creds = Mysigner::CredentialResolver.resolve_play(options: .to_h) warn "[mysigner] Google Play credentials source: #{creds.source}" if $stderr.respond_to?(:tty?) && $stderr.tty? creds rescue Mysigner::CredentialResolver::CredentialNotFoundError, Mysigner::CredentialResolver::AmbiguousCredentialsError => e say "✗ No local Google Play credentials found via `mysigner onboard --local-only` or other sources:\n#{e.}", :red exit 1 end |
#valid_ios_udid?(udid) ⇒ Boolean
Client-side UDID validity check for iOS devices. Matches the two formats Apple uses: 25-character alphanumeric (older devices pre- iPhone X) and 40-character hex, optionally with a single dash after the first 8 chars (newer). Also rejects obviously synthetic values (all zeros, single-character repeats) that Apple’s dev-portal sandbox has been known to accept even though they can never match a real device.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 46 def valid_ios_udid?(udid) return false if udid.nil? || udid.strip.empty? normalized = udid.strip.upcase # 25-char legacy UDID: alphanumeric legacy = normalized.match?(/\A[0-9A-F]{25}\z/) # 40-char modern UDID: hex, optional dash after first 8 chars modern_plain = normalized.match?(/\A[0-9A-F]{40}\z/) modern_dashed = normalized.match?(/\A[0-9A-F]{8}-[0-9A-F]{16}\z/) # 8-16 form some tools emit (older spec) modern_full_dashed = normalized.match?(/\A[0-9A-F]{8}-[0-9A-F]{32}\z/) # 8-32 (what xcrun outputs) return false unless legacy || modern_plain || modern_dashed || modern_full_dashed hex_only = normalized.delete('-') # Reject trivially synthetic UDIDs. A real UDID has at least 4 # distinct hex characters among its 25/40 positions; "000…" or # "AAAA…" or "012345…" style sequences flunk that. distinct = hex_only.chars.uniq.size return false if distinct < 4 true end |
#with_timing(_label) ⇒ Object
Helper for timing operations
8 9 10 11 12 13 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 8 def with_timing(_label) start = Time.now result = yield duration = Time.now - start [result, duration] end |