Module: Mysigner::CLI::Concerns::ActionableSuggestions

Included in:
Mysigner::CLI
Defined in:
lib/mysigner/cli/concerns/actionable_suggestions.rb

Overview

Provides actionable error messages with specific suggestions for common failure scenarios users encounter

Constant Summary collapse

IOS_ERROR_PATTERNS =

Error categories for iOS/App Store Connect

{
  # Build not found / processing
  build_not_found: {
    patterns: [
      /no.*build.*found/i,
      /build.*not.*found/i,
      /no.*processed.*build/i,
      /waiting.*for.*build/i
    ],
    title: 'Build Not Found',
    suggestions: [
      'Upload a build first: mysigner ship testflight',
      'If already uploaded, wait 5-15 minutes for Apple to process it',
      'Use --wait flag to poll for processing: mysigner ship appstore --wait',
      'Sync your builds: mysigner sync ios'
    ]
  },

  # Build still processing
  build_processing: {
    patterns: [
      /still.*processing/i,
      /processing.*complete/i,
      /not.*ready/i
    ],
    title: 'Build Still Processing',
    suggestions: [
      'Apple typically takes 5-15 minutes to process builds',
      'Use --wait flag to automatically wait: mysigner ship appstore --wait',
      'Check App Store Connect for processing status',
      'Increase timeout: mysigner ship appstore --wait --asc-timeout-seconds 1800'
    ]
  },

  # Profile issues
  profile_expired: {
    patterns: [
      /profile.*expired/i,
      /provisioning.*expired/i,
      /certificate.*expired/i
    ],
    title: 'Expired Profile or Certificate',
    suggestions: [
      'List your profiles: mysigner profiles',
      'Check expiration dates in My Signer dashboard',
      'Regenerate expired profiles in Apple Developer Portal',
      'Download fresh profile: mysigner profile download <ID>'
    ]
  },

  profile_not_found: {
    patterns: [
      /profile.*not.*found/i,
      /no.*provisioning.*profile/i,
      /missing.*profile/i,
      /unable.*find.*profile/i
    ],
    title: 'Provisioning Profile Not Found',
    suggestions: [
      'List available profiles: mysigner profiles',
      'Sync profiles from Apple: mysigner sync ios',
      'Create profile in Apple Developer Portal',
      'Check if profile is for correct Bundle ID'
    ]
  },

  # Certificate issues
  certificate_not_found: {
    patterns: [
      /certificate.*not.*found/i,
      /no.*signing.*certificate/i,
      /missing.*certificate/i
    ],
    title: 'Signing Certificate Not Found',
    suggestions: [
      'List certificates: mysigner certificates',
      'Download and install: mysigner certificate download <ID>',
      'Check Keychain Access for installed certificates',
      "Run 'mysigner doctor' to diagnose signing issues"
    ]
  },

  # Bundle ID issues
  bundle_id_mismatch: {
    patterns: [
      /bundle.*id.*mismatch/i,
      /bundle.*identifier.*not.*match/i,
      /app.*id.*not.*found/i
    ],
    title: 'Bundle ID Issue',
    suggestions: [
      'Verify Bundle ID in Xcode matches Apple Developer Portal',
      'List Bundle IDs: mysigner bundleid list',
      'Register new Bundle ID: mysigner bundleid register <ID>',
      'Check My Signer dashboard for registered apps'
    ]
  },

  # App not found
  app_not_found: {
    patterns: [
      /app.*with.*bundle.*id.*not.*found/i,
      /app.*not.*found/i,
      /unable.*find.*app/i
    ],
    title: 'App Not Found',
    suggestions: [
      'Ensure app exists in App Store Connect',
      'Create app in App Store Connect first',
      'Verify Bundle ID matches: check your Xcode project',
      'Sync from App Store Connect: mysigner sync ios'
    ]
  },

  # Version issues
  version_exists: {
    patterns: [
      /version.*already.*exists/i,
      /duplicate.*version/i
    ],
    title: 'Version Already Exists',
    suggestions: [
      'Increment version in Xcode (CFBundleShortVersionString)',
      'Or increment build number (CFBundleVersion)',
      'Check existing versions in App Store Connect'
    ]
  },

  # Submission issues
  missing_metadata: {
    patterns: [
      /missing.*required.*field/i,
      /what's.*new.*required/i,
      /support.*url.*required/i,
      /cannot.*submit.*missing/i
    ],
    title: 'Missing App Store Metadata',
    suggestions: [
      'Configure release in My Signer dashboard',
      "Provide What's New via CLI: --whats-new \"Your text\"",
      'Ensure support URL is set in App Store Connect',
      'Complete app information in App Store Connect'
    ]
  },

  # Archive issues
  archive_not_found: {
    patterns: [
      /archive.*not.*found/i,
      /no.*xcarchive/i,
      /.xcarchive.*not.*found/i
    ],
    title: 'Archive Not Found',
    suggestions: [
      'Build your app first: mysigner build',
      'Or use: mysigner ship testflight (handles build automatically)',
      'Check if archive path is correct',
      'Verify Xcode build succeeded'
    ]
  },

  ipa_not_found: {
    patterns: [
      /ipa.*not.*found/i,
      /no.*ipa.*file/i
    ],
    title: 'IPA File Not Found',
    suggestions: [
      'Export IPA from archive: mysigner export <archive_path>',
      'Or use: mysigner ship testflight (handles export automatically)',
      'Check if export succeeded',
      'Verify export method matches profile type'
    ]
  }
}.freeze
ANDROID_ERROR_PATTERNS =

Error categories for Android/Google Play

{
  # Keystore issues
  keystore_not_found: {
    patterns: [
      /keystore.*not.*found/i,
      /no.*keystore/i,
      /missing.*keystore/i
    ],
    title: 'Keystore Not Found',
    suggestions: [
      'Upload keystore: mysigner keystore upload <path>',
      'List keystores: mysigner keystore list',
      'Download keystore: mysigner keystore download <ID>',
      'Check keystore path in build.gradle'
    ]
  },

  keystore_password: {
    patterns: [
      /keystore.*password/i,
      /wrong.*password/i,
      /incorrect.*password/i
    ],
    title: 'Keystore Password Issue',
    suggestions: [
      'Verify keystore password is correct',
      'Check password in My Signer dashboard',
      'Update keystore password: mysigner keystore update <ID>'
    ]
  },

  # Package not found (first upload)
  package_not_found: {
    patterns: [
      /package.*not.*found/i,
      /first.*build.*uploaded.*manually/i
    ],
    title: 'First Upload Required',
    suggestions: [
      'Google Play requires the FIRST build to be uploaded manually:',
      '  1. Build AAB: mysigner android build',
      '  2. Go to Play Console → Your App → Internal testing',
      "  3. Click 'Create release' and upload the AAB",
      '  4. Save the release (no need to roll out)',
      'After that, mysigner ship will work for future uploads'
    ]
  },

  # Version code conflict
  version_code_conflict: {
    patterns: [
      /version.*code.*already/i,
      /already.*used/i,
      /duplicate.*version.*code/i
    ],
    title: 'Version Code Conflict',
    suggestions: [
      'Version code already exists on Google Play',
      'Run the command again - mysigner auto-increments the version',
      'Or manually increment versionCode in build.gradle'
    ]
  },

  # Service account issues
  service_account_missing: {
    patterns: [
      /service.*account.*not.*found/i,
      /service.*account.*json.*not.*found/i,
      /no.*credentials/i
    ],
    title: 'Service Account Not Found',
    suggestions: [
      'Set up Google Play credentials in My Signer dashboard:',
      '  1. Go to Play Console → API access → Service accounts',
      '  2. Create a service account with Editor access',
      '  3. Download the JSON key',
      '  4. Upload to My Signer dashboard → Google Play Settings'
    ]
  },

  service_account_permission: {
    patterns: [
      /not.*authorized/i,
      /permission.*denied/i,
      /forbidden/i,
      /access.*denied/i
    ],
    title: 'Service Account Permission Denied',
    suggestions: [
      'Service account lacks required permissions',
      'In Play Console → API access:',
      '  1. Find your service account',
      "  2. Click 'Manage Play Console permissions'",
      "  3. Grant 'Admin' or 'Release manager' access to the app",
      'Note: Permission changes take ~15 minutes to propagate'
    ]
  },

  # Track setup issues
  track_not_setup: {
    patterns: [
      /precondition.*failed/i,
      /precondition.*check.*failed/i,
      /track.*not.*ready/i
    ],
    title: 'Track Not Set Up in Play Console',
    suggestions: [
      'Complete track setup in Google Play Console:',
      '  For PRODUCTION: Complete store listing, content rating, pricing',
      '  For BETA/ALPHA: Create testing track and add testers',
      '  For INTERNAL: Add internal testers',
      'Your AAB was uploaded - go to Play Console to finish setup'
    ]
  },

  # AAB issues
  aab_not_found: {
    patterns: [
      /aab.*not.*found/i,
      /no.*aab.*file/i,
      /app.*bundle.*not.*found/i
    ],
    title: 'AAB File Not Found',
    suggestions: [
      'Build your Android app first: mysigner android build',
      'Or use: mysigner ship internal (handles build automatically)',
      'Check if Gradle build succeeded',
      'Verify AAB path in build output'
    ]
  },

  # Signing issues
  signing_mismatch: {
    patterns: [
      /signing.*key.*mismatch/i,
      /wrong.*key/i,
      /incorrect.*signature/i
    ],
    title: 'Signing Key Mismatch',
    suggestions: [
      'AAB is signed with a different key than expected',
      "Verify keystore matches what's registered in Play Console",
      'Check active keystore: mysigner keystore list --active',
      'If using Google Play App Signing, upload the correct upload key'
    ]
  }
}.freeze
API_ERROR_PATTERNS =

API/Connection error patterns

{
  plan_upgrade_required: {
    patterns: [
      /plan_upgrade_required/i,
      /upgrade.*required/i,
      /requires?.*(free|pro|team|paid)/i
    ],
    title: 'Plan Upgrade Required',
    suggestions: [
      'This feature is blocked by your current plan',
      'Upgrade from the My Signer pricing page or dashboard to continue',
      "Use 'mysigner switch' if you need to change to a different organization first"
    ]
  },

  quota_exhausted: {
    patterns: [
      /quota_exhausted/i,
      /quota.*exceeded/i,
      /limit.*reached/i,
      /storage.*limit/i,
      /upload.*limit/i
    ],
    title: 'Plan Limit Reached',
    suggestions: [
      'Your organization has reached a plan limit for this feature',
      'Free up capacity or upgrade your plan in the My Signer dashboard',
      'Check the pricing page for the next plan tier and included limits'
    ]
  },

  seat_limit_reached: {
    patterns: [
      /seat.*limit/i,
      /invite.*limit/i,
      /member.*limit/i
    ],
    title: 'Seat Limit Reached',
    suggestions: [
      'Your organization has reached its member or invite limit',
      'Remove unused members or invitations, or upgrade to a plan with more seats',
      "Use 'mysigner orgs' or the dashboard to confirm you are in the correct organization"
    ]
  },

  rate_limited: {
    patterns: [
      /rate.*limit/i,
      /too.*many.*requests/i,
      /429/
    ],
    title: 'Rate Limited',
    suggestions: [
      'Too many API requests - wait a moment and try again',
      'If problem persists, check API status'
    ]
  },

  server_error: {
    patterns: [
      /server.*error/i,
      /internal.*error/i,
      /500|502|503|504/
    ],
    title: 'Server Error',
    suggestions: [
      'My Signer server encountered an error',
      'Try again in a few moments',
      'If problem persists, check service status or contact support'
    ]
  },

  timeout: {
    patterns: [
      /timeout/i,
      /timed.*out/i
    ],
    title: 'Request Timeout',
    suggestions: [
      'Request took too long to complete',
      'Check your network connection',
      'Try again - this may be temporary'
    ]
  }
}.freeze
QUICK_REFERENCE =

Quick reference suggestions for common operations

{
  sync: {
    ios: 'mysigner sync ios',
    android: 'mysigner sync android'
  },
  profiles: 'mysigner profiles',
  certificates: 'mysigner certificates',
  keystores: 'mysigner keystore list',
  doctor: 'mysigner doctor',
  onboard: 'mysigner onboard',
  login: 'mysigner login'
}.freeze

Instance Method Summary collapse

Instance Method Details

#actionable_error_lookup_text(error_or_message) ⇒ Object



520
521
522
523
524
525
526
527
528
# File 'lib/mysigner/cli/concerns/actionable_suggestions.rb', line 520

def actionable_error_lookup_text(error_or_message)
  return error_or_message.to_s unless error_or_message.respond_to?(:message)

  [
    (error_or_message.error_code if error_or_message.respond_to?(:error_code)),
    error_or_message.message,
    (error_or_message.suggestion if error_or_message.respond_to?(:suggestion))
  ].compact.join(' ')
end

#display_error_with_suggestions(error, platform: nil, context: {}) ⇒ Object

Enhanced error display that includes suggestions



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/mysigner/cli/concerns/actionable_suggestions.rb', line 480

def display_error_with_suggestions(error, platform: nil, context: {})
  say ''
  say '=' * 80, :red
  say "#{context[:title] || 'Error'}", :red
  say '=' * 80, :red
  say ''
  say "Error: #{error.message}", :red
  say ''

  if error.respond_to?(:suggestion) && error.suggestion
    say "Suggestion: #{error.suggestion}", :yellow
    say ''
  end

  # Show actionable suggestions if available
  show_actionable_suggestions(error, platform: platform)

  # Show additional context
  say "Archive saved at: #{context[:archive_path]}", :yellow if context[:archive_path] && File.exist?(context[:archive_path])

  say "IPA saved at: #{context[:ipa_path]}", :yellow if context[:ipa_path] && File.exist?(context[:ipa_path])

  say "AAB saved at: #{context[:aab_path]}", :yellow if context[:aab_path] && File.exist?(context[:aab_path])

  # Debug info
  if ENV['DEBUG']
    say ''
    say 'Debug info:', :yellow
    say "  Error class: #{error.class}", :yellow
    say '  Backtrace:', :yellow
    error.backtrace&.first(5)&.each do |line|
      say "    #{line}", :yellow
    end
  else
    say '💡 For more details, run with DEBUG=1', :yellow
  end

  say ''
end

#find_suggestions_for_error(error_message, platform: nil) ⇒ Object

Find matching error pattern and return suggestions



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/mysigner/cli/concerns/actionable_suggestions.rb', line 423

def find_suggestions_for_error(error_message, platform: nil)
  patterns = case platform
             when :ios, :apple
               IOS_ERROR_PATTERNS.merge(API_ERROR_PATTERNS)
             when :android
               ANDROID_ERROR_PATTERNS.merge(API_ERROR_PATTERNS)
             else
               IOS_ERROR_PATTERNS.merge(ANDROID_ERROR_PATTERNS).merge(API_ERROR_PATTERNS)
             end

  patterns.each_value do |error_info|
    return error_info if error_info[:patterns].any? { |pattern| error_message =~ pattern }
  end

  nil
end

#format_actionable_suggestions(error_info) ⇒ Object

Format actionable suggestions for display



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/mysigner/cli/concerns/actionable_suggestions.rb', line 441

def format_actionable_suggestions(error_info)
  return nil unless error_info

  lines = []
  lines << ''
  lines << ('=' * 70)
  lines << "  💡 #{error_info[:title]}: How to fix"
  lines << ('=' * 70)
  lines << ''

  error_info[:suggestions].each do |suggestion|
    # Preserve indentation for multi-line suggestions
    lines << if suggestion.start_with?('  ')
               "  #{suggestion}"
             else
               "#{suggestion}"
             end
  end

  lines << ''
  lines << '  📚 More help:'
  lines << "    • Run 'mysigner doctor' to check your setup"
  lines << "    • Run 'mysigner help <command>' for command options"
  lines << ''

  lines.join("\n")
end

#quick_ref(operation, platform: nil) ⇒ Object

Get quick reference command



545
546
547
548
549
550
# File 'lib/mysigner/cli/concerns/actionable_suggestions.rb', line 545

def quick_ref(operation, platform: nil)
  ref = QUICK_REFERENCE[operation]
  return ref unless ref.is_a?(Hash)

  platform ? ref[platform] : ref[:ios]
end

#show_actionable_suggestions(error_or_message, platform: nil) ⇒ Object

Display actionable suggestions for an error (helper for CLI commands)



470
471
472
473
474
475
476
477
# File 'lib/mysigner/cli/concerns/actionable_suggestions.rb', line 470

def show_actionable_suggestions(error_or_message, platform: nil)
  error_info = find_suggestions_for_error(actionable_error_lookup_text(error_or_message), platform: platform)
  return false unless error_info

  output = format_actionable_suggestions(error_info)
  say output, :cyan if output
  true
end