require 'openssl' require 'base64' require 'tempfile' # OpenSSL 3.x rejects PKCS#8 keys via EC.new ("invalid curve name"). # Prepend a wrapper that falls back to PKey.read for both formats. module OpenSSLECNewFix def new(arg = nil, *rest) super rescue OpenSSL::PKey::ECError raise if arg.nil? || !arg.is_a?(String) OpenSSL::PKey.read(arg) end end OpenSSL::PKey::EC.singleton_class.prepend(OpenSSLECNewFix) def normalize_p8_key(raw) candidates = [] cleaned = raw.gsub("\r", "") with_newlines = cleaned.gsub('\n', "\n") candidates << ["raw (newlines normalized)", with_newlines] if with_newlines.include?("BEGIN") b64_body = with_newlines.gsub(/-----(?:BEGIN|END)[^-]+-----/, '').gsub(/\s+/, '') rewrapped = "-----BEGIN PRIVATE KEY-----\n#{b64_body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" candidates << ["rewrapped PEM", rewrapped] end unless with_newlines.include?("BEGIN") body = cleaned.strip.gsub(/\s+/, '') pem = "-----BEGIN PRIVATE KEY-----\n#{body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" candidates << ["headerless body → PEM", pem] end begin decoded = Base64.decode64(cleaned.strip) if decoded.include?("BEGIN") decoded_clean = decoded.gsub("\r", "").gsub('\n', "\n") candidates << ["base64→PEM", decoded_clean] b64_body = decoded_clean.gsub(/-----(?:BEGIN|END)[^-]+-----/, '').gsub(/\s+/, '') rewrapped = "-----BEGIN PRIVATE KEY-----\n#{b64_body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" candidates << ["base64→PEM rewrapped", rewrapped] elsif decoded.length.between?(32, 256) candidates << ["base64→DER", decoded] der_pem = "-----BEGIN PRIVATE KEY-----\n#{Base64.strict_encode64(decoded).scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" candidates << ["base64→DER→PEM", der_pem] end rescue StandardError # not valid base64 end begin double = Base64.decode64(Base64.decode64(cleaned.strip).strip) if double.include?("BEGIN") candidates << ["double-base64→PEM", double.gsub("\r", "")] elsif double.length.between?(32, 256) der_pem = "-----BEGIN PRIVATE KEY-----\n#{Base64.strict_encode64(double).scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n" candidates << ["double-base64→DER→PEM", der_pem] end rescue StandardError # not double-encoded end candidates.each do |name, content| begin OpenSSL::PKey.read(content) UI.success("ASC key parsed with strategy: #{name}") return content rescue StandardError => e UI.message("Strategy '#{name}' failed: #{e.class} — #{e.message}") end end UI.error("=== ASC KEY DIAGNOSTIC (no key material shown) ===") UI.error("Raw byte length: #{raw.bytesize}") UI.error("Starts with BEGIN: #{raw.strip.start_with?('-----BEGIN')}") UI.error("Ends with -----: #{raw.strip.end_with?('-----')}") UI.error("Has real newlines: #{raw.include?("\n")}") UI.error("Has literal backslash-n: #{raw.include?('\\n')}") UI.error("Has carriage returns: #{raw.include?("\r")}") UI.error("Printable ASCII ratio: #{(raw.count(' -~').to_f / raw.bytesize * 100).round(1)}%") UI.error("Header (first 27 chars): #{raw[0..26]}") begin d = Base64.decode64(raw.strip) hex = d.bytes[0..15].map { |b| format('%02x', b) }.join(' ') UI.error("After base64 decode — length: #{d.bytesize}, first 16 bytes hex: #{hex}") rescue StandardError UI.error("base64 decode raised an exception") end UI.error("=== END DIAGNOSTIC ===") raise "Could not parse ASC_KEY_CONTENT in any known format. See diagnostics above." end default_platform(:ios) platform :ios do desc "Build and upload to TestFlight" lane :beta do setup_ci(force: true) key_pem = normalize_p8_key(ENV["ASC_KEY_CONTENT"]) key_path = File.join(Dir.tmpdir, "asc_api_key.p8") File.write(key_path, key_pem) app_store_connect_api_key( key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], key_filepath: key_path ) File.delete(key_path) if File.exist?(key_path) match( type: "appstore", readonly: true, keychain_name: "fastlane_tmp_keychain", keychain_password: "" ) update_code_signing_settings( use_automatic_signing: false, team_id: "9KAQYC653W", code_sign_identity: "Apple Distribution", profile_name: "match AppStore com.seahavenind.proposals", path: "ios/ProposalSystem.xcodeproj" ) node_path = sh("which node").strip xcode_env_path = File.join(__dir__, "..", "ios", ".xcode.env.local") File.write(xcode_env_path, "export NODE_BINARY=#{node_path}\n") UI.message("NODE_BINARY set to #{node_path} at #{xcode_env_path}") increment_build_number( build_number: ENV["GITHUB_RUN_NUMBER"], xcodeproj: "ios/ProposalSystem.xcodeproj" ) build_app( workspace: "ios/ProposalSystem.xcworkspace", scheme: "ProposalSystem", configuration: "Release", export_method: "app-store", export_team_id: "9KAQYC653W", output_directory: "build", output_name: "ProposalSystem.ipa", xcodebuild_formatter: "" ) upload_to_testflight(skip_waiting_for_build_processing: true) end end