diff --git a/Sources/VariantsCore/Factory/iOS/XCConfigFactory.swift b/Sources/VariantsCore/Factory/iOS/XCConfigFactory.swift index cbdeca51..99327ae3 100644 --- a/Sources/VariantsCore/Factory/iOS/XCConfigFactory.swift +++ b/Sources/VariantsCore/Factory/iOS/XCConfigFactory.swift @@ -207,11 +207,15 @@ class XCConfigFactory: XCFactory { let exportMethod = signing.exportMethod, let teamName = signing.teamName, !teamName.isEmpty else { return } - - let isDistribution = exportMethod == .appstore || exportMethod == .enterprise - let certType = isDistribution ? "Distribution" : "Development" + signingSettings[PListKey.provisioningProfile] = "$(V_MATCH_PROFILE)" - signingSettings[PListKey.codeSignIdentity] = "Apple \(certType): \(teamName) (\(teamID))" + + if signing.autoDetectSigningIdentity, + let fetchedSigningIdentity = signing.codeSigningIdentity { + signingSettings[PListKey.codeSignIdentity] = fetchedSigningIdentity + } else { + signingSettings[PListKey.codeSignIdentity] = "Apple \(exportMethod.certType): \(teamName) (\(teamID))" + } } let xcodeFactory = XcodeProjFactory() @@ -248,10 +252,12 @@ class XCConfigFactory: XCFactory { let exportMethod = signing.exportMethod, let teamName = signing.teamName, !teamName.isEmpty else { return } - - let isDistribution = exportMethod == .appstore || exportMethod == .enterprise - let certType = isDistribution ? "Distribution" : "Development" - signingSettings[PListKey.codeSignIdentity] = "Apple \(certType): \(teamName) (\(teamID))" + + if signing.autoDetectSigningIdentity, let fetchedSigningIdentity = signing.codeSigningIdentity { + signingSettings[PListKey.codeSignIdentity] = fetchedSigningIdentity + } else { + signingSettings[PListKey.codeSignIdentity] = "Apple \(exportMethod.certType): \(teamName) (\(teamID))" + } } let xcodeFactory = XcodeProjFactory() diff --git a/Sources/VariantsCore/Schemas/iOS/iOSSigning.swift b/Sources/VariantsCore/Schemas/iOS/iOSSigning.swift index 8a9b74ff..9d9c141c 100644 --- a/Sources/VariantsCore/Schemas/iOS/iOSSigning.swift +++ b/Sources/VariantsCore/Schemas/iOS/iOSSigning.swift @@ -14,6 +14,11 @@ struct iOSSigning: Codable, Equatable { let exportMethod: ExportMethod? let matchURL: String? let style: SigningStyle + let autoDetectSigningIdentity: Bool + + var codeSigningIdentity: String? { + fetchSigningCertificate() + } enum CodingKeys: String, CodingKey { case teamName = "team_name" @@ -21,6 +26,7 @@ struct iOSSigning: Codable, Equatable { case exportMethod = "export_method" case matchURL = "match_url" case style + case autoDetectSigningIdentity = "auto_detect_signing_identity" } init(from decoder: any Decoder) throws { @@ -30,14 +36,22 @@ struct iOSSigning: Codable, Equatable { self.exportMethod = try container.decodeIfPresent(ExportMethod.self, forKey: .exportMethod) self.matchURL = try container.decodeIfPresent(String.self, forKey: .matchURL) self.style = try container.decodeIfPresent(iOSSigning.SigningStyle.self, forKey: .style) ?? .manual + let signingIdentity = try container.decodeIfPresent(Bool.self, forKey: .autoDetectSigningIdentity) + self.autoDetectSigningIdentity = signingIdentity ?? true } - - init(teamName: String?, teamID: String?, exportMethod: ExportMethod?, matchURL: String?, style: SigningStyle) { + + init(teamName: String?, + teamID: String?, + exportMethod: ExportMethod?, + matchURL: String?, + style: SigningStyle, + autoDetectSigningIdentity: Bool) { self.teamName = teamName self.teamID = teamID self.exportMethod = exportMethod self.matchURL = matchURL self.style = style + self.autoDetectSigningIdentity = autoDetectSigningIdentity } } @@ -60,6 +74,14 @@ extension iOSSigning { return "match InHouse" } } + + var isDistribution: Bool { + self == .appstore || self == .enterprise + } + + var certType: String { + isDistribution ? "Distribution" : "Development" + } } enum SigningStyle: String, Codable { @@ -106,7 +128,8 @@ extension iOSSigning { teamID: lhs.teamID ?? rhs?.teamID, exportMethod: lhs.exportMethod ?? rhs?.exportMethod, matchURL: lhs.matchURL ?? rhs?.matchURL, - style: lhs.style) + style: lhs.style, + autoDetectSigningIdentity: lhs.autoDetectSigningIdentity) guard signing.teamName != nil else { throw iOSSigning.missingParameterError(CodingKeys.teamName) } guard signing.teamID != nil else { throw iOSSigning.missingParameterError(CodingKeys.teamID) } @@ -115,3 +138,33 @@ extension iOSSigning { return signing } } + +extension iOSSigning { + private func fetchSigningCertificate() -> String? { + guard let teamID else { return nil } + + do { + let output = try Bash("security", arguments: "find-identity", "-v", "-p", "codesigning") + .capture() + + guard let output else { return nil } + let lines = output.split(separator: "\n") + + let matches = lines.compactMap { line -> String? in + guard line.contains(teamID) else { return nil } + + if let teamName, !line.contains(teamName) { return nil } + if let certType = exportMethod?.certType.lowercased(), + !line.contains(certType) { return nil } + + let components = line.split(separator: "\"", maxSplits: 2, omittingEmptySubsequences: false) + guard components.count > 1 else { return nil } + + return String(components[1]) + } + return matches.first + } catch { + return nil + } + } +} diff --git a/Templates/ios/variants-template.yml b/Templates/ios/variants-template.yml index fb8bc059..3735d522 100644 --- a/Templates/ios/variants-template.yml +++ b/Templates/ios/variants-template.yml @@ -40,7 +40,12 @@ ios: # match_url: "git@github.com:sample/match.git" team_name: "iPhone Distribution" team_id: "AB1234567D" - + + # Should Variant try to auto detect signing identity + # if set to true, Variant will use the team_id, team_name and export_method + # to detect code signing identity from the Keychain Access + # default value is `true` + auto_detect_signing_identity: true # # custom: - Not required. # @@ -100,7 +105,12 @@ ios: team_name: "iPhone Distribution" team_id: "AB1234567D" export_method: "appstore" - + + # Should Variant try to auto detect signing identity + # if set to true, Variant will use the team_id, team_name and export_method + # to detect code signing identity from the Keychain Access + # default value is `true` + auto_detect_signing_identity: true # ---------------------------------------------------------------------- # custom: - Not required. # diff --git a/Tests/VariantsCoreTests/FastlaneParametersFactoryTests.swift b/Tests/VariantsCoreTests/FastlaneParametersFactoryTests.swift index 41ad65f1..cccbbf6b 100644 --- a/Tests/VariantsCoreTests/FastlaneParametersFactoryTests.swift +++ b/Tests/VariantsCoreTests/FastlaneParametersFactoryTests.swift @@ -136,7 +136,12 @@ class FastlaneParametersFactoryTests: XCTestCase { bundleID: nil, globalCustomProperties: nil, variantCustomProperties: nil, - globalSigning: iOSSigning(teamName: "", teamID: "", exportMethod: .appstore, matchURL: "", style: .manual), + globalSigning: iOSSigning(teamName: "", + teamID: "", + exportMethod: .appstore, + matchURL: "", + style: .manual, + autoDetectSigningIdentity: true), debugSigning: nil, releaseSigning: nil, globalPostSwitchScript: "echo global", diff --git a/Tests/VariantsCoreTests/VariantsFileFactoryTests.swift b/Tests/VariantsCoreTests/VariantsFileFactoryTests.swift index c347d64f..91b112a5 100644 --- a/Tests/VariantsCoreTests/VariantsFileFactoryTests.swift +++ b/Tests/VariantsCoreTests/VariantsFileFactoryTests.swift @@ -54,7 +54,12 @@ class VariantsFileFactoryTests: XCTestCase { variantCustomProperties: [ CustomProperty(name: "PROPERTY_A", value: "VALUE_A", destination: .project), CustomProperty(name: "PROPERTY_B", value: "VALUE_B", destination: .project)], - globalSigning: iOSSigning(teamName: "", teamID: "", exportMethod: .appstore, matchURL: "", style: .manual), + globalSigning: iOSSigning(teamName: "", + teamID: "", + exportMethod: .appstore, + matchURL: "", + style: .manual, + autoDetectSigningIdentity: true), debugSigning: nil, releaseSigning: nil, globalPostSwitchScript: "echo global", diff --git a/Tests/VariantsCoreTests/iOSSigningTests.swift b/Tests/VariantsCoreTests/iOSSigningTests.swift index d011268f..a94d1561 100644 --- a/Tests/VariantsCoreTests/iOSSigningTests.swift +++ b/Tests/VariantsCoreTests/iOSSigningTests.swift @@ -25,12 +25,14 @@ final class iOSSigningTests: XCTestCase { teamID: nil, exportMethod: .appstore, matchURL: "url", - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) let signing1 = iOSSigning(teamName: nil, teamID: "new id", exportMethod: .development, matchURL: nil, - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) do { let result = try signing ~ signing1 @@ -48,12 +50,14 @@ final class iOSSigningTests: XCTestCase { teamID: nil, exportMethod: .appstore, matchURL: "url", - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) let signing1 = iOSSigning(teamName: nil, teamID: "new id", exportMethod: .development, matchURL: "new url", - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) let expectedError = RuntimeError(""" Missing: 'signing.team_name' At least one variant doesn't contain 'signing.team_name' in its configuration. @@ -74,12 +78,14 @@ final class iOSSigningTests: XCTestCase { teamID: nil, exportMethod: .appstore, matchURL: "url", - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) let signing1 = iOSSigning(teamName: "Name", teamID: nil, exportMethod: .development, matchURL: "new url", - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) let expectedError = RuntimeError(""" Missing: 'signing.team_id' At least one variant doesn't contain 'signing.team_id' in its configuration. @@ -100,7 +106,8 @@ final class iOSSigningTests: XCTestCase { teamID: nil, exportMethod: .enterprise, matchURL: "url", - style: .manual) + style: .manual, + autoDetectSigningIdentity: true) let expected = [CustomProperty(name: "TEAMNAME", value: "NAME", destination: .fastlane), CustomProperty(name: "EXPORTMETHOD", value: "match InHouse", destination: .fastlane), @@ -121,7 +128,7 @@ final class iOSSigningTests: XCTestCase { } func testOnlyGlobalSigning() { - let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual) + let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual, autoDetectSigningIdentity: true) let unnamedVariant = makeUnnamedVariant(signing: nil, debugSigning: nil, releaseSigning: nil) guard let variant = try? iOSVariant(from: unnamedVariant, name: "", globalCustomProperties: nil, globalSigning: globalSigning, globalPostSwitchScript: nil) @@ -132,8 +139,8 @@ final class iOSSigningTests: XCTestCase { } func testGlobalAndVariantSigning() { - let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual) - let variantSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual) + let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual, autoDetectSigningIdentity: true) + let variantSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: false) let unnamedVariant = makeUnnamedVariant(signing: variantSigning, debugSigning: nil, releaseSigning: nil) guard let variant = try? iOSVariant(from: unnamedVariant, name: "", globalCustomProperties: nil, globalSigning: globalSigning, globalPostSwitchScript: nil) @@ -144,8 +151,8 @@ final class iOSSigningTests: XCTestCase { } func testGlobalAndVariantReleaseSigning() { - let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual) - let variantReleaseSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual) + let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual, autoDetectSigningIdentity: true) + let variantReleaseSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: false) let unnamedVariant = makeUnnamedVariant(signing: nil, debugSigning: nil, releaseSigning: variantReleaseSigning) guard let variant = try? iOSVariant(from: unnamedVariant, name: "", globalCustomProperties: nil, globalSigning: globalSigning, globalPostSwitchScript: nil) @@ -156,8 +163,8 @@ final class iOSSigningTests: XCTestCase { } func testGlobalAndVariantDebugSigning() { - let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual) - let variantDebugSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual) + let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual, autoDetectSigningIdentity: true) + let variantDebugSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: false) let unnamedVariant = makeUnnamedVariant(signing: nil, debugSigning: variantDebugSigning, releaseSigning: nil) guard let variant = try? iOSVariant(from: unnamedVariant, name: "", globalCustomProperties: nil, globalSigning: globalSigning, globalPostSwitchScript: nil) @@ -168,11 +175,11 @@ final class iOSSigningTests: XCTestCase { } func testGlobalAndVariantReleaseDebugSigning() { - let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual) - let variantDebugSigning = iOSSigning(teamName: "variant debug team name", teamID: "variant_debug_team_id", - exportMethod: .appstore, matchURL: "variant match url", style: .manual) - let variantReleaseSigning = iOSSigning(teamName: "variant release team name", teamID: "variant_release_team_id", - exportMethod: .appstore, matchURL: "variant match url", style: .manual) + let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual, autoDetectSigningIdentity: true) + let variantDebugSigning = iOSSigning(teamName: "variant debug team name", teamID: "variant_debug_team_id", + exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: true) + let variantReleaseSigning = iOSSigning(teamName: "variant release team name", teamID: "variant_release_team_id", + exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: false) let unnamedVariant = makeUnnamedVariant(signing: nil, debugSigning: variantDebugSigning, releaseSigning: variantReleaseSigning) guard let variant = try? iOSVariant(from: unnamedVariant, name: "", globalCustomProperties: nil, globalSigning: globalSigning, globalPostSwitchScript: nil) @@ -183,9 +190,9 @@ final class iOSSigningTests: XCTestCase { } func testGlobalAndVariantSigningAndDebugSigning() { - let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual) - let variantSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual) - let variantDebugSigning = iOSSigning(teamName: "variant debug team name", teamID: "variant_debug_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual) + let globalSigning = iOSSigning(teamName: "global team name", teamID: "global_team_id", exportMethod: .appstore, matchURL: "global match url", style: .manual, autoDetectSigningIdentity: true) + let variantSigning = iOSSigning(teamName: "variant team name", teamID: "variant_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: true) + let variantDebugSigning = iOSSigning(teamName: "variant debug team name", teamID: "variant_debug_team_id", exportMethod: .appstore, matchURL: "variant match url", style: .manual, autoDetectSigningIdentity: true) let unnamedVariant = makeUnnamedVariant(signing: variantSigning, debugSigning: variantDebugSigning, releaseSigning: nil) guard let variant = try? iOSVariant(from: unnamedVariant, name: "", globalCustomProperties: nil, globalSigning: globalSigning, globalPostSwitchScript: nil) diff --git a/Tests/VariantsCoreTests/iOSTargetExtensionTests.swift b/Tests/VariantsCoreTests/iOSTargetExtensionTests.swift index ef6b66cd..23dc9a48 100644 --- a/Tests/VariantsCoreTests/iOSTargetExtensionTests.swift +++ b/Tests/VariantsCoreTests/iOSTargetExtensionTests.swift @@ -12,7 +12,7 @@ import XCTest @testable import VariantsCore class iOSTargetExtensionTests: XCTestCase { - private let validSigning = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: "git@github.com:sample/match.git", style: .manual) + private let validSigning = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: "git@github.com:sample/match.git", style: .manual, autoDetectSigningIdentity: true) private let target = iOSTarget(name: "Target Name", app_icon: "AppIcon", bundleId: "com.Company.ValidName", testTarget: "ValidNameTests", source: iOSSource(path: "", info: "", config: "")) func testTargetExtensionCreationWithBundleSuffix() { diff --git a/Tests/VariantsCoreTests/iOSVariantTests.swift b/Tests/VariantsCoreTests/iOSVariantTests.swift index 1e431b68..7fd26a55 100644 --- a/Tests/VariantsCoreTests/iOSVariantTests.swift +++ b/Tests/VariantsCoreTests/iOSVariantTests.swift @@ -14,7 +14,7 @@ import XCTest @testable import VariantsCore class iOSVariantTests: XCTestCase { - private let validSigning = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: "git@github.com:sample/match.git", style: .manual) + private let validSigning = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: "git@github.com:sample/match.git", style: .manual, autoDetectSigningIdentity: false) private let target = iOSTarget(name: "Target Name", app_icon: "AppIcon", bundleId: "com.Company.ValidName", testTarget: "ValidNameTests", source: iOSSource(path: "", info: "", config: "")) // MARK: - Initializer tests @@ -275,7 +275,7 @@ class iOSVariantTests: XCTestCase { "V_VERSION_NAME": "1.0.0", "V_VERSION_NUMBER": "0" ] - let signing = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: nil, style: .manual) + let signing = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: nil, style: .manual, autoDetectSigningIdentity: false) guard let variant = try? iOSVariant(name: "Beta", versionName: "1.0.0", versionNumber: 0, appIcon: nil, appName: nil, storeDestination: "appStore", idSuffix: "beta", bundleID: nil, globalCustomProperties: nil, variantCustomProperties: nil, globalSigning: signing, debugSigning: nil, releaseSigning: nil, globalPostSwitchScript: nil, variantPostSwitchScript: nil) @@ -297,7 +297,7 @@ class iOSVariantTests: XCTestCase { "V_VERSION_NAME": "1.0.0", "V_VERSION_NUMBER": "0" ] - let signing = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: nil, style: .manual) + let signing = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: nil, style: .manual, autoDetectSigningIdentity: false) guard let variant = try? iOSVariant(name: "Beta", versionName: "1.0.0", versionNumber: 0, appIcon: nil, appName: "App Marketing Name", storeDestination: "appStore", idSuffix: "beta", bundleID: nil, globalCustomProperties: nil, variantCustomProperties: nil, globalSigning: signing, debugSigning: nil, releaseSigning: nil, @@ -320,8 +320,9 @@ class iOSVariantTests: XCTestCase { "V_VERSION_NAME": "1.0.0", "V_VERSION_NUMBER": "0" ] - let signing = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: nil, style: .manual) - guard let variant = try? iOSVariant(name: "Beta", versionName: "1.0.0", versionNumber: 0, appIcon: nil, appName: nil, storeDestination: "appStore", + let signing = iOSSigning(teamName: "Signing Team Name", teamID: "AB12345CD", exportMethod: .appstore, matchURL: nil, style: .manual, autoDetectSigningIdentity: false) + guard let variant = try? iOSVariant(name: "Beta", versionName: "1.0.0", versionNumber: 0, appIcon: nil, appName: nil, + storeDestination: "appStore", idSuffix: "beta", bundleID: nil, globalCustomProperties: nil, variantCustomProperties: nil, globalSigning: signing, debugSigning: nil, releaseSigning: nil, globalPostSwitchScript: nil, variantPostSwitchScript: nil) else { diff --git a/docs/ios/IOS_SIGNING.md b/docs/ios/IOS_SIGNING.md index 3236c92e..78b1bfb8 100644 --- a/docs/ios/IOS_SIGNING.md +++ b/docs/ios/IOS_SIGNING.md @@ -8,6 +8,8 @@ The same priority applies to the debug signing `debug_signing` (from variant configuration) > `signing` (from variant configuration) > `signing` (from global configuration) +`auto_detect_signing_identity` boolean flag will determine if Variants should attempt to fetch the matching signing certificate from the Keychain Access automatically. If this fails, it will fall back to manual signing gracefully. Auto detect is enabled by default. + If no signing configuration is found, an error is thrown to the user so the `variants.yml` must be updated. ### Configuration example @@ -32,6 +34,7 @@ ios: team_name: "Beta Backbase B.V." team_id: "DEF7654321D" export_method: "appstore" + auto_detect_signing_identity: true staging: signing: match_url: "git@github.com:sample/match.git" @@ -53,6 +56,7 @@ ios: team_name: "Backbase B.V." team_id: "ABC1234567D" export_method: "appstore" + auto_detect_signing_identity: true ``` This is the output in Xcode and Matchfile: diff --git a/samples/ios/VariantsTestApp/variants.yml b/samples/ios/VariantsTestApp/variants.yml index 1004d79f..107f2601 100644 --- a/samples/ios/VariantsTestApp/variants.yml +++ b/samples/ios/VariantsTestApp/variants.yml @@ -86,6 +86,7 @@ ios: team_name: "Backbase B.V." team_id: "ABC1234567D" export_method: "appstore" + auto_detect_signing_identity: true custom: - name: custom_global_property