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
- #format_bytes(bytes) ⇒ Object
- #format_duration(seconds) ⇒ Object
- #load_config ⇒ Object
-
#local_only? ⇒ Boolean
Local-only mode is active if either the –local-only flag is set on this invocation OR MYSIGNER_LOCAL_ONLY is truthy in ENV.
-
#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
226 227 228 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 226 def @local_only_banner_emitted == true end |
.mark_banner_emitted! ⇒ Object
230 231 232 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 230 def @local_only_banner_emitted = true end |
.reset_banner! ⇒ Object
234 235 236 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 234 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 |
# 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) 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.)
212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 212 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
145 146 147 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 145 def error() say "✗ Error: #{}", :red 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 if either the –local-only flag is set on this invocation OR MYSIGNER_LOCAL_ONLY is truthy in ENV. Subsequent tickets gate credential-sending behavior on this.
152 153 154 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 152 def local_only? [:local_only] || 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.
196 197 198 199 200 201 202 203 204 205 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 196 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.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 161 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.
180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/mysigner/cli/concerns/helpers.rb', line 180 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 |