From 7645e81d532ed75abc541a80b94ee4cddb20a854 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Thu, 19 Mar 2026 13:48:01 -0500 Subject: [PATCH 01/38] Attempting to do CI to run unit tests on all PRs. Limiting to just iPhone for now --- .github/workflows/pull-request-ci.yml | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/pull-request-ci.yml diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml new file mode 100644 index 0000000..0d0bdb3 --- /dev/null +++ b/.github/workflows/pull-request-ci.yml @@ -0,0 +1,62 @@ +name: Pull Request CI + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: pull-request-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + name: Unit Tests (${{ matrix.scheme }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + scheme: + - libPhoneNumber + - libPhoneNumberGeocoding + - libPhoneNumberShortNumber + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Run unit tests + env: + SCHEME: ${{ matrix.scheme }} + run: | + set -eo pipefail + + xcodebuild \ + -project libPhoneNumber.xcodeproj \ + -scheme "$SCHEME" \ + -destination 'platform=iOS Simulator,name=iPhone,OS=latest' \ + CODE_SIGNING_ALLOWED=NO \ + test + + podspec-lint: + name: Podspec Lint (${{ matrix.podspec }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + podspec: + - libPhoneNumber-iOS.podspec + - libPhoneNumberGeocoding.podspec + - libPhoneNumberShortNumber.podspec + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Ensure CocoaPods is installed + run: | + if ! command -v pod >/dev/null; then + gem install cocoapods + fi + + - name: Lint podspec + run: pod lib lint "${{ matrix.podspec }}" --verbose From 96581e0f090d601ffef6215b8b21e57483d87909 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Thu, 19 Mar 2026 14:04:35 -0500 Subject: [PATCH 02/38] Specify "iPhone 17" --- .github/workflows/pull-request-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 0d0bdb3..7ba1c66 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -33,7 +33,7 @@ jobs: xcodebuild \ -project libPhoneNumber.xcodeproj \ -scheme "$SCHEME" \ - -destination 'platform=iOS Simulator,name=iPhone,OS=latest' \ + -destination 'platform=iOS Simulator,OS=latest,name=iPhone 17' \ CODE_SIGNING_ALLOWED=NO \ test From c82e40f42e6046df32c11f8313d963fbb8e850ee Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Thu, 19 Mar 2026 15:31:05 -0500 Subject: [PATCH 03/38] Adding .xctestplans into the xcode project file and remo --- libPhoneNumber.xcodeproj/project.pbxproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libPhoneNumber.xcodeproj/project.pbxproj b/libPhoneNumber.xcodeproj/project.pbxproj index a8dbd47..2409752 100755 --- a/libPhoneNumber.xcodeproj/project.pbxproj +++ b/libPhoneNumber.xcodeproj/project.pbxproj @@ -196,6 +196,9 @@ BB3F7C672EBD34DB0091CF5B /* _HeaderOnlyShim.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = _HeaderOnlyShim.c; sourceTree = ""; }; BB3F7C682EBD34DB0091CF5B /* NBGeneratedShortNumberMetaData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NBGeneratedShortNumberMetaData.h; sourceTree = ""; }; BB3F7C692EBD34DB0091CF5B /* NBShortNumberMetadataHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NBShortNumberMetadataHelper.h; sourceTree = ""; }; + BB4248A42F6C8AB300A7438E /* libPhoneNumberTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = libPhoneNumberTests.xctestplan; sourceTree = ""; }; + BB4248A52F6C8AEE00A7438E /* libPhoneNumberShortNumberTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = libPhoneNumberShortNumberTests.xctestplan; sourceTree = ""; }; + BB4248A72F6C8B5600A7438E /* libPhoneNumberGeocodingTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = libPhoneNumberGeocodingTests.xctestplan; sourceTree = ""; }; BB6A710C2EBD02F500292CA8 /* libPhoneNumberMetaDataForTesting.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = libPhoneNumberMetaDataForTesting.zip; sourceTree = ""; }; BB6A710D2EBD02F500292CA8 /* NBTestingMetaData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NBTestingMetaData.h; sourceTree = ""; }; BB6A71132EBD306400292CA8 /* _HeaderOnlyShim.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = _HeaderOnlyShim.c; sourceTree = ""; }; @@ -315,6 +318,7 @@ isa = PBXGroup; children = ( 0FC0D37A24A2A05E0087AFCF /* Info.plist */, + BB4248A72F6C8B5600A7438E /* libPhoneNumberGeocodingTests.xctestplan */, 94C9AF0E24B3AAF900469F54 /* NBPhoneNumberOfflineGeocoderTest.m */, CA55ED88296F51E0005E98A1 /* TestingSource.bundle */, ); @@ -325,6 +329,7 @@ isa = PBXGroup; children = ( 1485C52B1E06F4930092F541 /* Info.plist */, + BB4248A42F6C8AB300A7438E /* libPhoneNumberTests.xctestplan */, 1485C5231E06F4890092F541 /* NBAsYouTypeFormatterTest.m */, 0FAE11902037959800193503 /* NBPhoneNumberParsingPerfTest.m */, 1485C5251E06F4890092F541 /* NBPhoneNumberUtilTest.m */, @@ -351,6 +356,7 @@ isa = PBXGroup; children = ( 9407259A24BE768A0011AE05 /* Info.plist */, + BB4248A52F6C8AEE00A7438E /* libPhoneNumberShortNumberTests.xctestplan */, 940725AB24BF63050011AE05 /* NBShortNumberInfoTest.m */, 940725B024BF7B040011AE05 /* NBShortNumberTestHelper.h */, 940725B124BF7B040011AE05 /* NBShortNumberTestHelper.m */, From c93cd9f1ec19de9f83c731dbd1c7cb0b954b8e55 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Tue, 7 Apr 2026 17:45:06 -0500 Subject: [PATCH 04/38] Generate code coverage --- libPhoneNumber.xcodeproj/project.pbxproj | 65 ++++--------------- .../xcschemes/libPhoneNumber.xcscheme | 13 ++-- .../libPhoneNumberGeocoding.xcscheme | 22 +++---- .../libPhoneNumberShortNumber.xcscheme | 24 +++---- .../libPhoneNumberGeocodingTests.xctestplan | 14 +++- .../libPhoneNumberShortNumberTests.xctestplan | 13 +++- .../libPhoneNumberTests.xctestplan | 13 +++- 7 files changed, 73 insertions(+), 91 deletions(-) diff --git a/libPhoneNumber.xcodeproj/project.pbxproj b/libPhoneNumber.xcodeproj/project.pbxproj index 2409752..5311f0b 100755 --- a/libPhoneNumber.xcodeproj/project.pbxproj +++ b/libPhoneNumber.xcodeproj/project.pbxproj @@ -36,8 +36,8 @@ 8B0FD2FF1E4A88AC0049DF81 /* NSArray+NBAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0FD2FA1E4A88AC0049DF81 /* NSArray+NBAdditions.m */; }; 9407259424BE768A0011AE05 /* libPhoneNumberShortNumber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9407258B24BE768A0011AE05 /* libPhoneNumberShortNumber.framework */; }; 9407259B24BE768A0011AE05 /* libPhoneNumberShortNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = 9407258D24BE768A0011AE05 /* libPhoneNumberShortNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 940725A224BE769D0011AE05 /* libPhoneNumber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACBB851B7122AC0064B3BD /* libPhoneNumber.framework */; platformFilter = ios; }; - 940725A324BE769D0011AE05 /* libPhoneNumber.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACBB851B7122AC0064B3BD /* libPhoneNumber.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 940725A224BE769D0011AE05 /* libPhoneNumber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACBB851B7122AC0064B3BD /* libPhoneNumber.framework */; }; + 940725A324BE769D0011AE05 /* libPhoneNumber.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACBB851B7122AC0064B3BD /* libPhoneNumber.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 940725A924BE77420011AE05 /* NBShortNumberUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 940725A724BE77420011AE05 /* NBShortNumberUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; 940725AA24BE77420011AE05 /* NBShortNumberUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 940725A824BE77420011AE05 /* NBShortNumberUtil.m */; }; 940725AC24BF63050011AE05 /* NBShortNumberInfoTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 940725AB24BF63050011AE05 /* NBShortNumberInfoTest.m */; }; @@ -80,6 +80,7 @@ BB9B604A2EBE9A7800C48233 /* libPhoneNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = BB9B60492EBE9A7800C48233 /* libPhoneNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; BB9B604B2EBE9B0C00C48233 /* libPhoneNumber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACBB851B7122AC0064B3BD /* libPhoneNumber.framework */; platformFilter = ios; }; BB9B604C2EBE9B2200C48233 /* libPhoneNumber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACBB851B7122AC0064B3BD /* libPhoneNumber.framework */; platformFilter = ios; }; + BB9FA4CB2F85BF9800CCF4FC /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BB9FA4CA2F85BF8600CCF4FC /* libsqlite3.tbd */; }; BBF66CA92EBBF9B9005E3382 /* NBGeneratedPhoneNumberMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = BBF66CA82EBBF9B9005E3382 /* NBGeneratedPhoneNumberMetaData.m */; }; BBF66CAB2EBBF9E2005E3382 /* NBGeneratedShortNumberMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = BBF66CAA2EBBF9E2005E3382 /* NBGeneratedShortNumberMetaData.m */; }; BBF66CB02EBC0783005E3382 /* NSBundle+Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = BBF66CAD2EBC0783005E3382 /* NSBundle+Extensions.m */; }; @@ -216,6 +217,7 @@ BB9B60432EBE9A0500C48233 /* _HeaderOnlyShim.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = _HeaderOnlyShim.c; sourceTree = ""; }; BB9B60442EBE9A0500C48233 /* GeocodingMetaData.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = GeocodingMetaData.bundle; sourceTree = ""; }; BB9B60492EBE9A7800C48233 /* libPhoneNumber.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libPhoneNumber.h; sourceTree = ""; }; + BB9FA4CA2F85BF8600CCF4FC /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; BBCABBF32EE0E61E0011A4C7 /* updateProjectVersions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = updateProjectVersions.swift; sourceTree = ""; }; BBCABBF52EE0F6E90011A4C7 /* versionCommitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = versionCommitter.swift; sourceTree = ""; }; BBF66CA82EBBF9B9005E3382 /* NBGeneratedPhoneNumberMetaData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NBGeneratedPhoneNumberMetaData.m; sourceTree = ""; }; @@ -250,6 +252,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BB9FA4CB2F85BF9800CCF4FC /* libsqlite3.tbd in Frameworks */, 0FC0D36D24A29F680087AFCF /* libPhoneNumber.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -483,6 +486,7 @@ FD7A061F167715A0004BBEB6 /* Frameworks */ = { isa = PBXGroup; children = ( + BB9FA4CA2F85BF8600CCF4FC /* libsqlite3.tbd */, CAA5E78C29F84B7B00550AA7 /* Contacts.framework */, FD7A0624167715A0004BBEB6 /* CoreGraphics.framework */, FD7A0622167715A0004BBEB6 /* Foundation.framework */, @@ -714,9 +718,10 @@ FD7A0613167715A0004BBEB6 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0830; LastTestingUpgradeCheck = 0510; - LastUpgradeCheck = 1640; + LastUpgradeCheck = 2640; ORGANIZATIONNAME = Google; TargetAttributes = { 0FC0D36324A29F510087AFCF = { @@ -919,7 +924,6 @@ }; 940725A524BE769D0011AE05 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - platformFilter = ios; target = 34ACBB841B7122AC0064B3BD /* libPhoneNumber */; targetProxy = 940725A424BE769D0011AE05 /* PBXContainerItemProxy */; }; @@ -931,8 +935,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -940,13 +942,11 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = MQV8HVXR99; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberGeocoding/Info.plist; @@ -973,8 +973,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -983,14 +981,12 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = MQV8HVXR99; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberGeocoding/Info.plist; @@ -1017,9 +1013,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_CODE_COVERAGE = NO; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1027,8 +1020,6 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_GENERATE_TEST_COVERAGE_FILES = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberGeocodingTests/Info.plist; @@ -1050,9 +1041,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_CODE_COVERAGE = NO; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1062,8 +1050,6 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_GENERATE_TEST_COVERAGE_FILES = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberGeocodingTests/Info.plist; @@ -1085,14 +1071,12 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; CLANG_ANALYZER_NONNULL = YES; - CLANG_ENABLE_CODE_COVERAGE = NO; CLANG_ENABLE_MODULES = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_SUSPICIOUS_MOVES = YES; DEBUG_INFORMATION_FORMAT = dwarf; - GCC_GENERATE_TEST_COVERAGE_FILES = NO; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; @@ -1113,7 +1097,6 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = NO; CLANG_ANALYZER_NONNULL = YES; - CLANG_ENABLE_CODE_COVERAGE = NO; CLANG_ENABLE_MODULES = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1122,7 +1105,6 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; - GCC_GENERATE_TEST_COVERAGE_FILES = NO; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; @@ -1141,7 +1123,6 @@ 34ACBB9E1B7122AC0064B3BD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_MODULES = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -1149,9 +1130,7 @@ CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = NO; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1183,7 +1162,6 @@ 34ACBB9F1B7122AC0064B3BD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_ENABLE_MODULES = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -1191,17 +1169,14 @@ CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = NO; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -1230,7 +1205,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -1239,12 +1213,10 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 973LHT5R86; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberShortNumber/Info.plist; @@ -1271,7 +1243,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -1281,13 +1252,11 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 973LHT5R86; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberShortNumber/Info.plist; @@ -1314,8 +1283,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1324,7 +1291,6 @@ CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 973LHT5R86; - GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberShortNumberTests/Info.plist; @@ -1346,8 +1312,6 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -1358,7 +1322,6 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 973LHT5R86; ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = libPhoneNumberShortNumberTests/Info.plist; @@ -1400,15 +1363,14 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1.4.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; - GCC_GENERATE_TEST_COVERAGE_FILES = YES; - GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -1428,6 +1390,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 12.0; @@ -1462,13 +1425,12 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = YES; CURRENT_PROJECT_VERSION = 1.4.0; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_GENERATE_TEST_COVERAGE_FILES = YES; - GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -1481,6 +1443,7 @@ MARKETING_VERSION = 1.4.0; OTHER_CFLAGS = ""; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumber.xcscheme b/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumber.xcscheme index 4bab9f4..e19219d 100644 --- a/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumber.xcscheme +++ b/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumber.xcscheme @@ -1,10 +1,10 @@ + buildImplicitDependencies = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + diff --git a/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumberGeocoding.xcscheme b/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumberGeocoding.xcscheme index 440e888..7be2b76 100644 --- a/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumberGeocoding.xcscheme +++ b/libPhoneNumber.xcodeproj/xcshareddata/xcschemes/libPhoneNumberGeocoding.xcscheme @@ -1,10 +1,10 @@ + buildImplicitDependencies = "NO"> - - - - - - + + + + + LastUpgradeVersion = "2640" + version = "1.7"> + buildImplicitDependencies = "NO"> - - - - - - + + + + Date: Tue, 7 Apr 2026 18:01:29 -0500 Subject: [PATCH 05/38] Add code coverage results --- .github/workflows/pull-request-ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 7ba1c66..c5190d7 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -29,14 +29,24 @@ jobs: SCHEME: ${{ matrix.scheme }} run: | set -eo pipefail + mkdir -p TestResults xcodebuild \ -project libPhoneNumber.xcodeproj \ -scheme "$SCHEME" \ -destination 'platform=iOS Simulator,OS=latest,name=iPhone 17' \ + -resultBundlePath "TestResults/${SCHEME}.xcresult" \ + -enableCodeCoverage YES \ CODE_SIGNING_ALLOWED=NO \ test + - name: Upload unit test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: project-unit-tests-${{ matrix.scheme }} + path: TestResults/${{ matrix.scheme }}.xcresult + podspec-lint: name: Podspec Lint (${{ matrix.podspec }}) runs-on: macos-latest From 31c0f504b76a1f628e1f8502c9c056afd67c97d5 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 11:19:44 -0500 Subject: [PATCH 06/38] Upload xcresult as an xcresult Dynamically determine the newest iPhone model and run tests on that --- .github/workflows/pull-request-ci.yml | 38 +++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index c5190d7..c99a1e0 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -24,6 +24,40 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Resolve iPhone simulator destination + id: destination + run: | + set -eo pipefail + + destination_id="$( + xcrun simctl list devices --json | + python3 -c ' +import json +import re +import sys + +matches = [] +for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): + runtime_match = re.match(r"com\\.apple\\.CoreSimulator\\.SimRuntime\\.iOS-(\\d+)-(\\d+)", runtime) + if not runtime_match: + continue + + os_version = tuple(int(part) for part in runtime_match.groups()) + for entry in entries: + name = (entry.get("name") or "").strip() + model_match = re.match(r"iPhone\\s+(\\d+)\\b", name) + if entry.get("isAvailable") and entry.get("udid") and model_match: + matches.append((os_version, int(model_match.group(1)), entry["udid"])) + +if not matches: + raise SystemExit("No iPhone simulator destinations found") + +print(max(matches)[2]) +' + )" + + echo "destination=id=$destination_id" >> "$GITHUB_OUTPUT" + - name: Run unit tests env: SCHEME: ${{ matrix.scheme }} @@ -34,7 +68,7 @@ jobs: xcodebuild \ -project libPhoneNumber.xcodeproj \ -scheme "$SCHEME" \ - -destination 'platform=iOS Simulator,OS=latest,name=iPhone 17' \ + -destination "${{ steps.destination.outputs.destination }}" \ -resultBundlePath "TestResults/${SCHEME}.xcresult" \ -enableCodeCoverage YES \ CODE_SIGNING_ALLOWED=NO \ @@ -44,7 +78,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: project-unit-tests-${{ matrix.scheme }} + name: project-unit-tests-${{ matrix.scheme }}.xcresult path: TestResults/${{ matrix.scheme }}.xcresult podspec-lint: From 4c1efa3b900a05f287e98b50c0924566d69f4788 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 11:32:48 -0500 Subject: [PATCH 07/38] Attempting to indent the imbedded python script --- .github/workflows/pull-request-ci.yml | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index c99a1e0..8d09f48 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -32,28 +32,28 @@ jobs: destination_id="$( xcrun simctl list devices --json | python3 -c ' -import json -import re -import sys - -matches = [] -for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): - runtime_match = re.match(r"com\\.apple\\.CoreSimulator\\.SimRuntime\\.iOS-(\\d+)-(\\d+)", runtime) - if not runtime_match: - continue - - os_version = tuple(int(part) for part in runtime_match.groups()) - for entry in entries: - name = (entry.get("name") or "").strip() - model_match = re.match(r"iPhone\\s+(\\d+)\\b", name) - if entry.get("isAvailable") and entry.get("udid") and model_match: - matches.append((os_version, int(model_match.group(1)), entry["udid"])) - -if not matches: - raise SystemExit("No iPhone simulator destinations found") - -print(max(matches)[2]) -' + import json + import re + import sys + + matches = [] + for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): + runtime_match = re.match(r"com\\.apple\\.CoreSimulator\\.SimRuntime\\.iOS-(\\d+)-(\\d+)", runtime) + if not runtime_match: + continue + + os_version = tuple(int(part) for part in runtime_match.groups()) + for entry in entries: + name = (entry.get("name") or "").strip() + model_match = re.match(r"iPhone\\s+(\\d+)\\b", name) + if entry.get("isAvailable") and entry.get("udid") and model_match: + matches.append((os_version, int(model_match.group(1)), entry["udid"])) + + if not matches: + raise SystemExit("No iPhone simulator destinations found") + + print(max(matches)[2]) + ' )" echo "destination=id=$destination_id" >> "$GITHUB_OUTPUT" From fd2ac21928277403df35f4e90c0e4dda2e5ffe26 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 11:39:58 -0500 Subject: [PATCH 08/38] Debugging around the xcrun simctl command --- .github/workflows/pull-request-ci.yml | 40 +++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 8d09f48..b52284c 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -29,6 +29,16 @@ jobs: run: | set -eo pipefail + echo "macOS version:" + sw_vers + echo "xcodebuild path: $(xcode-select -p)" + echo "xcodebuild version:" + xcodebuild -version + echo "xcrun path: $(command -v xcrun)" + echo "simctl path: $(xcrun --find simctl)" + echo "Available runtimes:" + xcrun simctl list runtimes + destination_id="$( xcrun simctl list devices --json | python3 -c ' @@ -36,26 +46,50 @@ jobs: import re import sys + devices = json.load(sys.stdin).get("devices", {}) + print(f"Discovered {len(devices)} simulator runtime groups", file=sys.stderr) + matches = [] - for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): + for runtime, entries in devices.items(): + print(f"Inspecting runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) runtime_match = re.match(r"com\\.apple\\.CoreSimulator\\.SimRuntime\\.iOS-(\\d+)-(\\d+)", runtime) if not runtime_match: + print(" Skipping non-iOS runtime", file=sys.stderr) continue os_version = tuple(int(part) for part in runtime_match.groups()) for entry in entries: name = (entry.get("name") or "").strip() model_match = re.match(r"iPhone\\s+(\\d+)\\b", name) - if entry.get("isAvailable") and entry.get("udid") and model_match: + is_available = entry.get("isAvailable") + udid = entry.get("udid") + + if is_available and udid and model_match: + print( + f" Candidate: name={name!r} os={os_version} model={model_match.group(1)} udid={udid}", + file=sys.stderr, + ) matches.append((os_version, int(model_match.group(1)), entry["udid"])) + else: + print( + f" Ignoring device: name={name!r} available={is_available} " + f"has_udid={bool(udid)} matches_iPhone={bool(model_match)}", + file=sys.stderr, + ) if not matches: raise SystemExit("No iPhone simulator destinations found") - print(max(matches)[2]) + selected = max(matches) + print( + f"Selected destination: os={selected[0]} model={selected[1]} udid={selected[2]}", + file=sys.stderr, + ) + print(selected[2]) ' )" + echo "Resolved destination id: $destination_id" echo "destination=id=$destination_id" >> "$GITHUB_OUTPUT" - name: Run unit tests From 9ae206afe0e9591f70604b98fca8d673dc11faa8 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 11:44:11 -0500 Subject: [PATCH 09/38] Try to NOT escape out the back slashes in the regex strings --- .github/workflows/pull-request-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index b52284c..364f472 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -52,7 +52,7 @@ jobs: matches = [] for runtime, entries in devices.items(): print(f"Inspecting runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) - runtime_match = re.match(r"com\\.apple\\.CoreSimulator\\.SimRuntime\\.iOS-(\\d+)-(\\d+)", runtime) + runtime_match = re.match(r"com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)", runtime) if not runtime_match: print(" Skipping non-iOS runtime", file=sys.stderr) continue @@ -60,7 +60,7 @@ jobs: os_version = tuple(int(part) for part in runtime_match.groups()) for entry in entries: name = (entry.get("name") or "").strip() - model_match = re.match(r"iPhone\\s+(\\d+)\\b", name) + model_match = re.match(r"iPhone\s+(\d+)\b", name) is_available = entry.get("isAvailable") udid = entry.get("udid") From b9db29207acc7576bcd218857c311f1370d44f78 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 11:59:39 -0500 Subject: [PATCH 10/38] Trim up the output. --- .github/workflows/pull-request-ci.yml | 45 +++++---------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 364f472..d394591 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -29,16 +29,6 @@ jobs: run: | set -eo pipefail - echo "macOS version:" - sw_vers - echo "xcodebuild path: $(xcode-select -p)" - echo "xcodebuild version:" - xcodebuild -version - echo "xcrun path: $(command -v xcrun)" - echo "simctl path: $(xcrun --find simctl)" - echo "Available runtimes:" - xcrun simctl list runtimes - destination_id="$( xcrun simctl list devices --json | python3 -c ' @@ -46,50 +36,31 @@ jobs: import re import sys - devices = json.load(sys.stdin).get("devices", {}) - print(f"Discovered {len(devices)} simulator runtime groups", file=sys.stderr) - matches = [] - for runtime, entries in devices.items(): - print(f"Inspecting runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) + for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): runtime_match = re.match(r"com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)", runtime) if not runtime_match: - print(" Skipping non-iOS runtime", file=sys.stderr) continue + print(f"Runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) + os_version = tuple(int(part) for part in runtime_match.groups()) for entry in entries: name = (entry.get("name") or "").strip() model_match = re.match(r"iPhone\s+(\d+)\b", name) - is_available = entry.get("isAvailable") - udid = entry.get("udid") - - if is_available and udid and model_match: - print( - f" Candidate: name={name!r} os={os_version} model={model_match.group(1)} udid={udid}", - file=sys.stderr, - ) - matches.append((os_version, int(model_match.group(1)), entry["udid"])) - else: - print( - f" Ignoring device: name={name!r} available={is_available} " - f"has_udid={bool(udid)} matches_iPhone={bool(model_match)}", - file=sys.stderr, - ) + if entry.get("isAvailable") and entry.get("udid") and model_match: + matches.append((os_version, int(model_match.group(1)), model, entry["udid"])) + print(f" {name} - {os_version[0]}.{os_version[1]} ({udid})", file=sys.stderr) if not matches: raise SystemExit("No iPhone simulator destinations found") selected = max(matches) - print( - f"Selected destination: os={selected[0]} model={selected[1]} udid={selected[2]}", - file=sys.stderr, - ) - print(selected[2]) + print(f"Selected: {selected[2]} - {selected[0][0]}.{selected[0][1]} (selected[3])", file=sys.stderr) + print(selected[3]) ' )" - echo "Resolved destination id: $destination_id" echo "destination=id=$destination_id" >> "$GITHUB_OUTPUT" - name: Run unit tests From 36f078856ae2e23e790ba38ae16a1041fc8bef6b Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 12:00:46 -0500 Subject: [PATCH 11/38] Fix model name --- .github/workflows/pull-request-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index d394591..e40f3cb 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -49,7 +49,7 @@ jobs: name = (entry.get("name") or "").strip() model_match = re.match(r"iPhone\s+(\d+)\b", name) if entry.get("isAvailable") and entry.get("udid") and model_match: - matches.append((os_version, int(model_match.group(1)), model, entry["udid"])) + matches.append((os_version, int(model_match.group(1)), name, entry["udid"])) print(f" {name} - {os_version[0]}.{os_version[1]} ({udid})", file=sys.stderr) if not matches: From c696a833b15f7d0b5f955ddd463c15cdb76b89bb Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 12:03:52 -0500 Subject: [PATCH 12/38] no udid variable...dummy --- .github/workflows/pull-request-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index e40f3cb..40d938b 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -50,7 +50,7 @@ jobs: model_match = re.match(r"iPhone\s+(\d+)\b", name) if entry.get("isAvailable") and entry.get("udid") and model_match: matches.append((os_version, int(model_match.group(1)), name, entry["udid"])) - print(f" {name} - {os_version[0]}.{os_version[1]} ({udid})", file=sys.stderr) + print(f" {name} - {os_version[0]}.{os_version[1]} ({entry["udid"]})", file=sys.stderr) if not matches: raise SystemExit("No iPhone simulator destinations found") From 0f75a1889f02e7e4fbf5d06e2d0f994d91ff4328 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 12:06:15 -0500 Subject: [PATCH 13/38] I made myself laugh! --- .github/workflows/pull-request-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 40d938b..2da39f3 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -56,7 +56,7 @@ jobs: raise SystemExit("No iPhone simulator destinations found") selected = max(matches) - print(f"Selected: {selected[2]} - {selected[0][0]}.{selected[0][1]} (selected[3])", file=sys.stderr) + print(f"Selected: {selected[2]} - {selected[0][0]}.{selected[0][1]} ({selected[3]})", file=sys.stderr) print(selected[3]) ' )" From 2ca5e8298fd6c06b5c69a59b631ca2681b3c542b Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 12:27:32 -0500 Subject: [PATCH 14/38] Prefer arm64 --- .github/workflows/pull-request-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 2da39f3..1ac754f 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -61,7 +61,7 @@ jobs: ' )" - echo "destination=id=$destination_id" >> "$GITHUB_OUTPUT" + echo "destination=id=$destination_id,arch=arm64" >> "$GITHUB_OUTPUT" - name: Run unit tests env: From a6ecc7293405696e3bb836bbb0b3f90bb7d546e9 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 12:33:43 -0500 Subject: [PATCH 15/38] Update to cleanly support Node.js 24 (and 20) --- .github/workflows/pull-request-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 1ac754f..9a05174 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Resolve iPhone simulator destination id: destination @@ -81,7 +81,7 @@ jobs: - name: Upload unit test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: project-unit-tests-${{ matrix.scheme }}.xcresult path: TestResults/${{ matrix.scheme }}.xcresult @@ -99,7 +99,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Ensure CocoaPods is installed run: | From 232308f6dc8e3b5c70410696fe1235a786048168 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 13:04:02 -0500 Subject: [PATCH 16/38] Attempting to publish up the code coverage results for pull requests --- .coveralls.yml | 1 - .github/workflows/pull-request-ci.yml | 124 +++++++++++++++++++++++++- .slather.yml | 12 --- .travis.yml | 17 ---- README.md | 3 +- spec_helper.rb | 2 - 6 files changed, 122 insertions(+), 37 deletions(-) delete mode 100644 .coveralls.yml delete mode 100644 .slather.yml delete mode 100644 .travis.yml delete mode 100644 spec_helper.rb diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 9160059..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -service_name: travis-ci diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 9a05174..6d6e0c4 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -50,13 +50,13 @@ jobs: model_match = re.match(r"iPhone\s+(\d+)\b", name) if entry.get("isAvailable") and entry.get("udid") and model_match: matches.append((os_version, int(model_match.group(1)), name, entry["udid"])) - print(f" {name} - {os_version[0]}.{os_version[1]} ({entry["udid"]})", file=sys.stderr) + print(f" {name} (iOS {os_version[0]}.{os_version[1]}) [{entry["udid"]}]", file=sys.stderr) if not matches: raise SystemExit("No iPhone simulator destinations found") - selected = max(matches) - print(f"Selected: {selected[2]} - {selected[0][0]}.{selected[0][1]} ({selected[3]})", file=sys.stderr) + selected = max(matches); name=selected[2]; theOS=""+selected[0][0]+"."+selected[0][1] + print(f"Selected Simulator: {name} (iOS {theOS}) [{selected[3]}]", file=sys.stderr) print(selected[3]) ' )" @@ -79,6 +79,39 @@ jobs: CODE_SIGNING_ALLOWED=NO \ test + - name: Export code coverage data + if: always() + env: + SCHEME: ${{ matrix.scheme }} + run: | + set -eo pipefail + + result_bundle="TestResults/${SCHEME}.xcresult" + coverage_json="TestResults/${SCHEME}.xccov.json" + + echo "### Code Coverage" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + if [ ! -d "$result_bundle" ]; then + echo "Coverage report unavailable because \`$result_bundle\` was not created." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + if ! xcrun xccov view --archive --json "$result_bundle" > "$coverage_json"; then + echo "Coverage report unavailable because \`xccov\` could not parse \`$result_bundle\`." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + echo "Exported line-level coverage data for \`$SCHEME\`." >> "$GITHUB_STEP_SUMMARY" + + - name: Upload code coverage data + if: always() + uses: actions/upload-artifact@v6 + with: + name: unit-test-coverage-${{ matrix.scheme }} + path: TestResults/${{ matrix.scheme }}.xccov.json + if-no-files-found: ignore + - name: Upload unit test results if: always() uses: actions/upload-artifact@v6 @@ -86,6 +119,91 @@ jobs: name: project-unit-tests-${{ matrix.scheme }}.xcresult path: TestResults/${{ matrix.scheme }}.xcresult + coverage-summary: + name: Combined Code Coverage + runs-on: ubuntu-latest + needs: unit-tests + if: always() + + steps: + - name: Download code coverage data + continue-on-error: true + uses: actions/download-artifact@v5 + with: + pattern: unit-test-coverage-* + path: CoverageData + merge-multiple: true + + - name: Publish combined coverage summary + run: | + set -eo pipefail + shopt -s nullglob + + coverage_files=(CoverageData/*.xccov.json) + + echo "### Combined Code Coverage" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + if [ ${#coverage_files[@]} -eq 0 ]; then + echo "Combined coverage unavailable because no coverage data artifacts were downloaded." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + python3 - "${coverage_files[@]}" >> "$GITHUB_STEP_SUMMARY" <<'PY' + import json + import os + import sys + + combined = {} + per_scheme = [] + + for path in sys.argv[1:]: + with open(path) as handle: + report = json.load(handle) + + covered_lines = 0 + executable_lines = 0 + for file_path, entries in report.items(): + combined_lines = combined.setdefault(file_path, {}) + for entry in entries: + line_number = entry.get("line") + if line_number is None or not entry.get("isExecutable"): + continue + + is_covered = entry.get("executionCount", 0) > 0 + executable_lines += 1 + if is_covered: + covered_lines += 1 + + previous = combined_lines.get(line_number, False) + combined_lines[line_number] = previous or is_covered + + scheme_name = os.path.basename(path).replace(".xccov.json", "") + coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 + per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) + + combined_executable_lines = sum(len(lines) for lines in combined.values()) + combined_covered_lines = sum( + 1 for lines in combined.values() for is_covered in lines.values() if is_covered + ) + combined_coverage_percent = ( + combined_covered_lines / combined_executable_lines * 100 + if combined_executable_lines + else 0.0 + ) + + print("| Scope | Coverage | Covered Lines | Executable Lines |") + print("| --- | ---: | ---: | ---: |") + for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): + print( + f"| {scheme_name} | {coverage_percent:.2f}% | {covered_lines} | {executable_lines} |" + ) + print( + f"| Combined | {combined_coverage_percent:.2f}% | " + f"{combined_covered_lines} | {combined_executable_lines} |" + ) + PY + podspec-lint: name: Podspec Lint (${{ matrix.podspec }}) runs-on: macos-latest diff --git a/.slather.yml b/.slather.yml deleted file mode 100644 index b1c4036..0000000 --- a/.slather.yml +++ /dev/null @@ -1,12 +0,0 @@ -ci_service: travis_ci -coverage_service: coveralls -xcodeproj: libPhoneNumber.xcodeproj -ignore: - - libPhoneNumber/AppDelegate.m - - libPhoneNumber/main.m - - libPhoneNumber/NBPhoneMetaDataGenerator.m - - libPhoneNumber/NBPhoneMetaData.m - - libPhoneNumber/NBPhoneNumberDesc.m - - libPhoneNumber/NBMetadataHelper.m - - libPhoneNumber/NBNumberFormat.m - - libPhoneNumber/NBPhoneNumber.m diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 94d69eb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: objective-c - -before_install: -- gem install slather -N - -script: -- xcodebuild -project libPhoneNumber.xcodeproj -scheme libPhoneNumberiOS clean -sdk iphonesimulator -- xcodebuild -project libPhoneNumber.xcodeproj -scheme libPhoneNumberiOS -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES build-for-testing -- xctool -project libPhoneNumber.xcodeproj -scheme libPhoneNumberiOS run-tests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES - -- xcodebuild -project libPhoneNumber.xcodeproj -scheme libPhoneNumberGeocoding -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES build-for-testing -- xctool -project libPhoneNumber.xcodeproj -scheme libPhoneNumberGeocoding run-tests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES - -- xcodebuild -project libPhoneNumber.xcodeproj -scheme libPhoneNumberShortNumber -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES build-for-testing -- xctool -project libPhoneNumber.xcodeproj -scheme libPhoneNumberShortNumber run-tests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES - -after_success: slather diff --git a/README.md b/README.md index 3d91032..5e3d5fb 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![CocoaPods](https://img.shields.io/cocoapods/p/libPhoneNumber-iOS.svg?style=flat)](http://cocoapods.org/?q=libPhoneNumber-iOS) [![CocoaPods](https://img.shields.io/cocoapods/v/libPhoneNumber-iOS.svg?style=flat)](http://cocoapods.org/?q=libPhoneNumber-iOS) -[![Travis](https://travis-ci.org/iziz/libPhoneNumber-iOS.svg?branch=master)](https://travis-ci.org/iziz/libPhoneNumber-iOS) -[![Coveralls](https://coveralls.io/repos/iziz/libPhoneNumber-iOS/badge.svg?branch=master&service=github)](https://coveralls.io/github/iziz/libPhoneNumber-iOS?branch=master) +[![Pull Request CI](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/pull-request-ci.yml/badge.svg?branch=master)](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/pull-request-ci.yml) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) # **libPhoneNumber for iOS** diff --git a/spec_helper.rb b/spec_helper.rb deleted file mode 100644 index 54a6989..0000000 --- a/spec_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'coveralls' -Coveralls.wear! From cf1558082832ae3787a320501b8d7104860351e3 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 13:07:37 -0500 Subject: [PATCH 17/38] Better string manipulation --- .github/workflows/pull-request-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 6d6e0c4..f3657eb 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -50,12 +50,13 @@ jobs: model_match = re.match(r"iPhone\s+(\d+)\b", name) if entry.get("isAvailable") and entry.get("udid") and model_match: matches.append((os_version, int(model_match.group(1)), name, entry["udid"])) - print(f" {name} (iOS {os_version[0]}.{os_version[1]}) [{entry["udid"]}]", file=sys.stderr) + theOS=f"{os_version[0]}.{os_version[1]}" + print(f" {name} (iOS {theOS}) [{entry["udid"]}]", file=sys.stderr) if not matches: raise SystemExit("No iPhone simulator destinations found") - selected = max(matches); name=selected[2]; theOS=""+selected[0][0]+"."+selected[0][1] + selected = max(matches); name=selected[2]; theOS=f"{selected[0][0]}.{selected[0][1]}" print(f"Selected Simulator: {name} (iOS {theOS}) [{selected[3]}]", file=sys.stderr) print(selected[3]) ' From 9d84000fd87272beb21aef0e85d1782184391118 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 13:40:12 -0500 Subject: [PATCH 18/38] Clean up --- .github/workflows/pull-request-ci.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index f3657eb..b6d26ef 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -4,6 +4,11 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + issues: write + pull-requests: write + concurrency: group: pull-request-ci-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -50,14 +55,14 @@ jobs: model_match = re.match(r"iPhone\s+(\d+)\b", name) if entry.get("isAvailable") and entry.get("udid") and model_match: matches.append((os_version, int(model_match.group(1)), name, entry["udid"])) - theOS=f"{os_version[0]}.{os_version[1]}" - print(f" {name} (iOS {theOS}) [{entry["udid"]}]", file=sys.stderr) + the_os = f"{os_version[0]}.{os_version[1]}" + print(f" {name} (iOS {the_os}) [{entry['udid']}]", file=sys.stderr) if not matches: raise SystemExit("No iPhone simulator destinations found") - selected = max(matches); name=selected[2]; theOS=f"{selected[0][0]}.{selected[0][1]}" - print(f"Selected Simulator: {name} (iOS {theOS}) [{selected[3]}]", file=sys.stderr) + selected = max(matches); name = selected[2]; the_os = f"{selected[0][0]}.{selected[0][1]}" + print(f"Selected Simulator: {name} (iOS {the_os}) [{selected[3]}]", file=sys.stderr) print(selected[3]) ' )" @@ -129,7 +134,7 @@ jobs: steps: - name: Download code coverage data continue-on-error: true - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: pattern: unit-test-coverage-* path: CoverageData From b7a7f1497931a25d9dfa94846bff3a15e980c501 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 13:40:32 -0500 Subject: [PATCH 19/38] Attempt to publish coverage to the pull request --- .github/workflows/pull-request-ci.yml | 115 +++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index b6d26ef..53c66f6 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -145,17 +145,29 @@ jobs: set -eo pipefail shopt -s nullglob + mkdir -p CoverageData coverage_files=(CoverageData/*.xccov.json) + summary_file="CoverageData/combined-coverage-summary.md" - echo "### Combined Code Coverage" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Coverage data directory contents:" + ls -la CoverageData || true + echo "Coverage JSON files found: ${#coverage_files[@]}" + for coverage_file in "${coverage_files[@]}"; do + echo "Coverage artifact: $coverage_file" + wc -c "$coverage_file" + done if [ ${#coverage_files[@]} -eq 0 ]; then - echo "Combined coverage unavailable because no coverage data artifacts were downloaded." >> "$GITHUB_STEP_SUMMARY" + { + echo "### Combined Code Coverage" + echo + echo "Combined coverage unavailable because no coverage data artifacts were downloaded." + } > "$summary_file" + cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" exit 0 fi - python3 - "${coverage_files[@]}" >> "$GITHUB_STEP_SUMMARY" <<'PY' + python3 - "${coverage_files[@]}" > "$summary_file" <<'PY' import json import os import sys @@ -164,6 +176,7 @@ jobs: per_scheme = [] for path in sys.argv[1:]: + print(f"Parsing coverage file: {path}", file=sys.stderr) with open(path) as handle: report = json.load(handle) @@ -187,6 +200,10 @@ jobs: scheme_name = os.path.basename(path).replace(".xccov.json", "") coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) + print( + f"Scheme {scheme_name}: covered={covered_lines} executable={executable_lines} coverage={coverage_percent:.2f}%", + file=sys.stderr, + ) combined_executable_lines = sum(len(lines) for lines in combined.values()) combined_covered_lines = sum( @@ -198,6 +215,13 @@ jobs: else 0.0 ) + print( + f"Combined coverage: covered={combined_covered_lines} executable={combined_executable_lines} coverage={combined_coverage_percent:.2f}%", + file=sys.stderr, + ) + + print("### Combined Code Coverage") + print() print("| Scope | Coverage | Covered Lines | Executable Lines |") print("| --- | ---: | ---: | ---: |") for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): @@ -210,6 +234,89 @@ jobs: ) PY + echo "Combined coverage summary:" + cat "$summary_file" + cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" + + - name: Publish combined coverage comment to pull request + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + run: | + set -eo pipefail + + summary_file="CoverageData/combined-coverage-summary.md" + if [ ! -f "$summary_file" ]; then + echo "No combined coverage summary file was generated; skipping PR comment." + exit 0 + fi + + comment_body_file="$(mktemp)" + { + echo '' + echo + cat "$summary_file" + } > "$comment_body_file" + + comments_json="$(mktemp)" + curl -fsSL \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ + > "$comments_json" + + comment_id="$( + python3 - "$comments_json" <<'PY' + import json + import sys + + marker = "" + with open(sys.argv[1]) as handle: + comments = json.load(handle) + + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + PY + )" + + payload_file="$(mktemp)" + python3 - "$comment_body_file" "$payload_file" <<'PY' + import json + import sys + + with open(sys.argv[1]) as handle: + body = handle.read() + + with open(sys.argv[2], "w") as handle: + json.dump({"body": body}, handle) + PY + + if [ -n "$comment_id" ]; then + echo "Updating existing coverage PR comment: $comment_id" + curl -fsSL \ + -X PATCH \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + --data @"$payload_file" \ + "https://api.github.com/repos/$REPOSITORY/issues/comments/$comment_id" \ + > /dev/null + else + echo "Creating new coverage PR comment" + curl -fsSL \ + -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + --data @"$payload_file" \ + "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ + > /dev/null + fi + podspec-lint: name: Podspec Lint (${{ matrix.podspec }}) runs-on: macos-latest From dd4443ecd5857a495bbdc30a7c3f52f36c9bb39c Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 13:45:13 -0500 Subject: [PATCH 20/38] more clean up --- .github/workflows/pull-request-ci.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 53c66f6..17bf29b 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -51,12 +51,13 @@ jobs: os_version = tuple(int(part) for part in runtime_match.groups()) for entry in entries: - name = (entry.get("name") or "").strip() + name = (entry["name"] or "").strip() model_match = re.match(r"iPhone\s+(\d+)\b", name) - if entry.get("isAvailable") and entry.get("udid") and model_match: - matches.append((os_version, int(model_match.group(1)), name, entry["udid"])) - the_os = f"{os_version[0]}.{os_version[1]}" - print(f" {name} (iOS {the_os}) [{entry['udid']}]", file=sys.stderr) + udid = entry["udid"] + if entry["isAvailable"] and udid and model_match: + matches.append((os_version, int(model_match.group(1)), name, udid)) + the_os = f"{os_version[0]}.{os_version[1]}" + print(f" {name} (iOS {the_os}) [{udid}]", file=sys.stderr) if not matches: raise SystemExit("No iPhone simulator destinations found") @@ -185,8 +186,8 @@ jobs: for file_path, entries in report.items(): combined_lines = combined.setdefault(file_path, {}) for entry in entries: - line_number = entry.get("line") - if line_number is None or not entry.get("isExecutable"): + line_number = entry["line"] + if line_number is None or not entry["isExecutable"]: continue is_covered = entry.get("executionCount", 0) > 0 From 565897606c5de936d80e303c49ff3f36c42cb8d1 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 14:31:47 -0500 Subject: [PATCH 21/38] Try to fix the combined code coverage logic --- .github/workflows/pull-request-ci.yml | 65 +++++++++++---------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 17bf29b..0ab5286 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -254,47 +254,36 @@ jobs: exit 0 fi - comment_body_file="$(mktemp)" - { - echo '' - echo - cat "$summary_file" - } > "$comment_body_file" - - comments_json="$(mktemp)" - curl -fsSL \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ - > "$comments_json" - comment_id="$( - python3 - "$comments_json" <<'PY' - import json - import sys - - marker = "" - with open(sys.argv[1]) as handle: - comments = json.load(handle) - - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - PY + curl -fsSL \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | + python3 -c ' + import json + import sys + + marker = "" + comments = json.load(sys.stdin) + + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" - payload_file="$(mktemp)" - python3 - "$comment_body_file" "$payload_file" <<'PY' - import json - import sys + payload="$( + python3 -c ' + import json + import sys - with open(sys.argv[1]) as handle: - body = handle.read() + with open(sys.argv[1]) as handle: + body = "\n\n" + handle.read() - with open(sys.argv[2], "w") as handle: - json.dump({"body": body}, handle) - PY + print(json.dumps({"body": body})) + ' "$summary_file" + )" if [ -n "$comment_id" ]; then echo "Updating existing coverage PR comment: $comment_id" @@ -303,7 +292,7 @@ jobs: -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ -H "Content-Type: application/json" \ - --data @"$payload_file" \ + --data "$payload" \ "https://api.github.com/repos/$REPOSITORY/issues/comments/$comment_id" \ > /dev/null else @@ -313,7 +302,7 @@ jobs: -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ -H "Content-Type: application/json" \ - --data @"$payload_file" \ + --data "$payload" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ > /dev/null fi From 8b41cb59c70972c321f20565cda290952e4e4a7c Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 14:45:19 -0500 Subject: [PATCH 22/38] Muck with the indentation --- .github/workflows/pull-request-ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 0ab5286..5f52c83 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -259,18 +259,18 @@ jobs: -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | - python3 -c ' - import json - import sys - - marker = "" - comments = json.load(sys.stdin) - - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' + python3 -c ' + import json + import sys + + marker = "" + comments = json.load(sys.stdin) + + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" payload="$( From 86fb1f092db8e36eefdb44b950a5bfa265cf122e Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 14:45:33 -0500 Subject: [PATCH 23/38] Clean up the code coverage summary handling logic --- .github/workflows/pull-request-ci.yml | 137 +++++++++++--------------- 1 file changed, 60 insertions(+), 77 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 5f52c83..9483272 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -150,14 +150,6 @@ jobs: coverage_files=(CoverageData/*.xccov.json) summary_file="CoverageData/combined-coverage-summary.md" - echo "Coverage data directory contents:" - ls -la CoverageData || true - echo "Coverage JSON files found: ${#coverage_files[@]}" - for coverage_file in "${coverage_files[@]}"; do - echo "Coverage artifact: $coverage_file" - wc -c "$coverage_file" - done - if [ ${#coverage_files[@]} -eq 0 ]; then { echo "### Combined Code Coverage" @@ -168,75 +160,66 @@ jobs: exit 0 fi - python3 - "${coverage_files[@]}" > "$summary_file" <<'PY' - import json - import os - import sys - - combined = {} - per_scheme = [] - - for path in sys.argv[1:]: - print(f"Parsing coverage file: {path}", file=sys.stderr) - with open(path) as handle: - report = json.load(handle) - - covered_lines = 0 - executable_lines = 0 - for file_path, entries in report.items(): - combined_lines = combined.setdefault(file_path, {}) - for entry in entries: - line_number = entry["line"] - if line_number is None or not entry["isExecutable"]: - continue - - is_covered = entry.get("executionCount", 0) > 0 - executable_lines += 1 - if is_covered: - covered_lines += 1 - - previous = combined_lines.get(line_number, False) - combined_lines[line_number] = previous or is_covered - - scheme_name = os.path.basename(path).replace(".xccov.json", "") - coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 - per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) - print( - f"Scheme {scheme_name}: covered={covered_lines} executable={executable_lines} coverage={coverage_percent:.2f}%", - file=sys.stderr, - ) - - combined_executable_lines = sum(len(lines) for lines in combined.values()) - combined_covered_lines = sum( - 1 for lines in combined.values() for is_covered in lines.values() if is_covered - ) - combined_coverage_percent = ( - combined_covered_lines / combined_executable_lines * 100 - if combined_executable_lines - else 0.0 - ) - - print( - f"Combined coverage: covered={combined_covered_lines} executable={combined_executable_lines} coverage={combined_coverage_percent:.2f}%", - file=sys.stderr, - ) - - print("### Combined Code Coverage") - print() - print("| Scope | Coverage | Covered Lines | Executable Lines |") - print("| --- | ---: | ---: | ---: |") - for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): - print( - f"| {scheme_name} | {coverage_percent:.2f}% | {covered_lines} | {executable_lines} |" - ) - print( - f"| Combined | {combined_coverage_percent:.2f}% | " - f"{combined_covered_lines} | {combined_executable_lines} |" - ) - PY - - echo "Combined coverage summary:" - cat "$summary_file" + python3 -c ' + import json + import os + import sys + + combined = {} + per_scheme = [] + + for path in sys.argv[1:-1]: + print(f"Processing coverage file: {path}", file=sys.stderr) + with open(path) as handle: + report = json.load(handle) + + covered_lines = 0 + executable_lines = 0 + for file_path, entries in report.items(): + combined_lines = combined.setdefault(file_path, {}) + for entry in entries: + line_number = entry["line"] + if line_number is None or not entry["isExecutable"]: + continue + + is_covered = entry.get("executionCount", 0) > 0 + executable_lines += 1 + if is_covered: + covered_lines += 1 + + combined_lines[line_number] = combined_lines.get(line_number, False) or is_covered + + scheme_name = os.path.basename(path).replace(".xccov.json", "") + coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 + per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) + + combined_executable_lines = sum(len(lines) for lines in combined.values()) + combined_covered_lines = sum( + 1 for lines in combined.values() for is_covered in lines.values() if is_covered + ) + combined_coverage_percent = ( + combined_covered_lines / combined_executable_lines * 100 + if combined_executable_lines + else 0.0 + ) + + with open(sys.argv[-1], "w") as handle: + print("### Combined Code Coverage", file=handle) + print(file=handle) + print("| Scope | Coverage | Covered Lines | Executable Lines |", file=handle) + print("| --- | ---: | ---: | ---: |", file=handle) + for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): + print( + f"| {scheme_name} | {coverage_percent:.2f}% | {covered_lines} | {executable_lines} |", + file=handle, + ) + print( + f"| Combined | {combined_coverage_percent:.2f}% | " + f"{combined_covered_lines} | {combined_executable_lines} |", + file=handle, + ) + ' "${coverage_files[@]}" "$summary_file" + cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" - name: Publish combined coverage comment to pull request From 83d71f6b4242c5fec5956819d1caabae5f117ebd Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 14:53:33 -0500 Subject: [PATCH 24/38] Hopefully fix the indentation issue --- .github/workflows/pull-request-ci.yml | 114 +++++++++++++------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 9483272..9915b4f 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -161,64 +161,64 @@ jobs: fi python3 -c ' - import json - import os - import sys - - combined = {} - per_scheme = [] - - for path in sys.argv[1:-1]: - print(f"Processing coverage file: {path}", file=sys.stderr) - with open(path) as handle: - report = json.load(handle) - - covered_lines = 0 - executable_lines = 0 - for file_path, entries in report.items(): - combined_lines = combined.setdefault(file_path, {}) - for entry in entries: - line_number = entry["line"] - if line_number is None or not entry["isExecutable"]: - continue - - is_covered = entry.get("executionCount", 0) > 0 - executable_lines += 1 - if is_covered: - covered_lines += 1 - - combined_lines[line_number] = combined_lines.get(line_number, False) or is_covered - - scheme_name = os.path.basename(path).replace(".xccov.json", "") - coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 - per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) - - combined_executable_lines = sum(len(lines) for lines in combined.values()) - combined_covered_lines = sum( - 1 for lines in combined.values() for is_covered in lines.values() if is_covered - ) - combined_coverage_percent = ( - combined_covered_lines / combined_executable_lines * 100 - if combined_executable_lines - else 0.0 + import json + import os + import sys + + combined = {} + per_scheme = [] + + for path in sys.argv[1:-1]: + print(f"Processing coverage file: {path}", file=sys.stderr) + with open(path) as handle: + report = json.load(handle) + + covered_lines = 0 + executable_lines = 0 + for file_path, entries in report.items(): + combined_lines = combined.setdefault(file_path, {}) + for entry in entries: + line_number = entry["line"] + if line_number is None or not entry["isExecutable"]: + continue + + is_covered = entry.get("executionCount", 0) > 0 + executable_lines += 1 + if is_covered: + covered_lines += 1 + + combined_lines[line_number] = combined_lines.get(line_number, False) or is_covered + + scheme_name = os.path.basename(path).replace(".xccov.json", "") + coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 + per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) + + combined_executable_lines = sum(len(lines) for lines in combined.values()) + combined_covered_lines = sum( + 1 for lines in combined.values() for is_covered in lines.values() if is_covered + ) + combined_coverage_percent = ( + combined_covered_lines / combined_executable_lines * 100 + if combined_executable_lines + else 0.0 + ) + + with open(sys.argv[-1], "w") as handle: + print("### Combined Code Coverage", file=handle) + print(file=handle) + print("| Scope | Coverage | Covered Lines | Executable Lines |", file=handle) + print("| --- | ---: | ---: | ---: |", file=handle) + for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): + print( + f"| {scheme_name} | {coverage_percent:.2f}% | {covered_lines} | {executable_lines} |", + file=handle, + ) + print( + f"| Combined | {combined_coverage_percent:.2f}% | " + f"{combined_covered_lines} | {combined_executable_lines} |", + file=handle, ) - - with open(sys.argv[-1], "w") as handle: - print("### Combined Code Coverage", file=handle) - print(file=handle) - print("| Scope | Coverage | Covered Lines | Executable Lines |", file=handle) - print("| --- | ---: | ---: | ---: |", file=handle) - for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): - print( - f"| {scheme_name} | {coverage_percent:.2f}% | {covered_lines} | {executable_lines} |", - file=handle, - ) - print( - f"| Combined | {combined_coverage_percent:.2f}% | " - f"{combined_covered_lines} | {combined_executable_lines} |", - file=handle, - ) - ' "${coverage_files[@]}" "$summary_file" + ' "${coverage_files[@]}" "$summary_file" cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" From a4dc7cc59fba4105271be618ca172822bd345a4e Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:02:17 -0500 Subject: [PATCH 25/38] Fixing more indents --- .github/workflows/pull-request-ci.yml | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 9915b4f..493c271 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -243,29 +243,29 @@ jobs: -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | python3 -c ' - import json - import sys + import json + import sys - marker = "" - comments = json.load(sys.stdin) + marker = "" + comments = json.load(sys.stdin) - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" payload="$( python3 -c ' - import json - import sys + import json + import sys - with open(sys.argv[1]) as handle: - body = "\n\n" + handle.read() + with open(sys.argv[1]) as handle: + body = "\n\n" + handle.read() - print(json.dumps({"body": body})) - ' "$summary_file" + print(json.dumps({"body": body})) + ' "$summary_file" )" if [ -n "$comment_id" ]; then From dfa08828284b743a9d5e25461ff120da152b37db Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:19:06 -0500 Subject: [PATCH 26/38] remove some indents --- .github/workflows/pull-request-ci.yml | 52 +++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 493c271..be75772 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -43,24 +43,24 @@ jobs: matches = [] for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): - runtime_match = re.match(r"com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)", runtime) - if not runtime_match: - continue - - print(f"Runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) - - os_version = tuple(int(part) for part in runtime_match.groups()) - for entry in entries: - name = (entry["name"] or "").strip() - model_match = re.match(r"iPhone\s+(\d+)\b", name) - udid = entry["udid"] - if entry["isAvailable"] and udid and model_match: - matches.append((os_version, int(model_match.group(1)), name, udid)) - the_os = f"{os_version[0]}.{os_version[1]}" - print(f" {name} (iOS {the_os}) [{udid}]", file=sys.stderr) + runtime_match = re.match(r"com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)", runtime) + if not runtime_match: + continue + + print(f"Runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) + + os_version = tuple(int(part) for part in runtime_match.groups()) + for entry in entries: + name = (entry["name"] or "").strip() + model_match = re.match(r"iPhone\s+(\d+)\b", name) + udid = entry["udid"] + if entry["isAvailable"] and udid and model_match: + matches.append((os_version, int(model_match.group(1)), name, udid)) + the_os = f"{os_version[0]}.{os_version[1]}" + print(f" {name} (iOS {the_os}) [{udid}]", file=sys.stderr) if not matches: - raise SystemExit("No iPhone simulator destinations found") + raise SystemExit("No iPhone simulator destinations found") selected = max(matches); name = selected[2]; the_os = f"{selected[0][0]}.{selected[0][1]}" print(f"Selected Simulator: {name} (iOS {the_os}) [{selected[3]}]", file=sys.stderr) @@ -242,18 +242,18 @@ jobs: -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | - python3 -c ' - import json - import sys + python3 -c ' + import json + import sys - marker = "" - comments = json.load(sys.stdin) + marker = "" + comments = json.load(sys.stdin) - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" payload="$( From 283d219be99e72a343afecc1ef15310dcfffd719 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:23:15 -0500 Subject: [PATCH 27/38] trying again --- .github/workflows/pull-request-ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index be75772..95e6229 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -242,18 +242,18 @@ jobs: -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | - python3 -c ' - import json - import sys + python3 -c ' + import sys + import json - marker = "" - comments = json.load(sys.stdin) + marker = "" + comments = json.load(sys.stdin) - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" payload="$( From ec9138bd3fcf0e7d0c1b636219a1525fc72365e2 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:31:25 -0500 Subject: [PATCH 28/38] getting desperate --- .github/workflows/pull-request-ci.yml | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 95e6229..b5def9e 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -237,23 +237,33 @@ jobs: exit 0 fi + testing_test="$( + echo "How are you" | + python3 -c ' + import re + import sys + + print(f"I am a test, testing things - {sys.stdin}") + ' + )" + comment_id="$( curl -fsSL \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | - python3 -c ' - import sys - import json + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | + python3 -c ' + import sys + import json - marker = "" - comments = json.load(sys.stdin) + marker = "" + comments = json.load(sys.stdin) - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" payload="$( From 91c2426286e52a1209a9267efcd631b3a7898786 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:37:29 -0500 Subject: [PATCH 29/38] WTF?!?!? --- .github/workflows/pull-request-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index b5def9e..e43e9d5 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -240,13 +240,15 @@ jobs: testing_test="$( echo "How are you" | python3 -c ' - import re - import sys + import re + import sys - print(f"I am a test, testing things - {sys.stdin}") + print(f"I am a test, testing things - {sys.stdin}", file=sys.stderr) ' )" + echo "$testing_test" + comment_id="$( curl -fsSL \ -H "Authorization: Bearer $GITHUB_TOKEN" \ From ecfc277813d7674ece3028d99d854f929ba9cc48 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:48:22 -0500 Subject: [PATCH 30/38] getting ridiculous --- .github/workflows/pull-request-ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index e43e9d5..ca9afb0 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -240,14 +240,15 @@ jobs: testing_test="$( echo "How are you" | python3 -c ' - import re - import sys - - print(f"I am a test, testing things - {sys.stdin}", file=sys.stderr) + import re + import sys + value = sys.stdin.read() + print(f"I am a test, testing things - {value}", file=sys.stderr) + print(f"Output testing - {value}") ' )" - echo "$testing_test" + echo "Output: $testing_test" comment_id="$( curl -fsSL \ From 2335b8de21ad5d7b2cde847cb3c4781203434af7 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 15:53:34 -0500 Subject: [PATCH 31/38] Going Plaid! --- .github/workflows/pull-request-ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index ca9afb0..f5bfaa8 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -255,18 +255,18 @@ jobs: -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | - python3 -c ' - import sys - import json + python3 -c ' + import sys + import json - marker = "" - comments = json.load(sys.stdin) + marker = "" + comments = json.load(sys.stdin) - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' )" payload="$( From 505397f29b9bcf52d89cc49de2183d437ca8cf6c Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 16:01:02 -0500 Subject: [PATCH 32/38] All this indenting! --- .github/workflows/pull-request-ci.yml | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index f5bfaa8..66c9a94 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -237,27 +237,14 @@ jobs: exit 0 fi - testing_test="$( - echo "How are you" | - python3 -c ' - import re - import sys - value = sys.stdin.read() - print(f"I am a test, testing things - {value}", file=sys.stderr) - print(f"Output testing - {value}") - ' - )" - - echo "Output: $testing_test" - comment_id="$( curl -fsSL \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | python3 -c ' - import sys import json + import sys marker = "" comments = json.load(sys.stdin) @@ -266,18 +253,18 @@ jobs: if marker in comment.get("body", ""): print(comment["id"]) break - ' + ' )" payload="$( python3 -c ' - import json - import sys + import json + import sys - with open(sys.argv[1]) as handle: - body = "\n\n" + handle.read() + with open(sys.argv[1]) as handle: + body = "\n\n" + handle.read() - print(json.dumps({"body": body})) + print(json.dumps({"body": body})) ' "$summary_file" )" From 9250d5a057cb72698f1a72e65ea36f8d2ddddf06 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 16:16:54 -0500 Subject: [PATCH 33/38] Getting fancy --- .github/workflows/pull-request-ci.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 66c9a94..baf2c1a 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -203,19 +203,29 @@ jobs: else 0.0 ) + def status_emoji(coverage_percent): + if coverage_percent < 60.0: + return "❌" + if coverage_percent < 75.0: + return "⚠️" + return "✅" + with open(sys.argv[-1], "w") as handle: print("### Combined Code Coverage", file=handle) print(file=handle) - print("| Scope | Coverage | Covered Lines | Executable Lines |", file=handle) - print("| --- | ---: | ---: | ---: |", file=handle) + print("| Scope | Coverage | Status |", file=handle) + print("| --- | ---: | --- |", file=handle) for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): print( - f"| {scheme_name} | {coverage_percent:.2f}% | {covered_lines} | {executable_lines} |", + f"| {scheme_name} | {coverage_percent:.2f}% | {status_emoji(coverage_percent)} |", file=handle, ) print( - f"| Combined | {combined_coverage_percent:.2f}% | " - f"{combined_covered_lines} | {combined_executable_lines} |", + "| **----------** | **----------:** | **---** |", + file=handle, + ) + print( + f"| **Combined** | **{combined_coverage_percent:.2f}%** | **{status_emoji(combined_coverage_percent)}** |", file=handle, ) ' "${coverage_files[@]}" "$summary_file" From 18b8f53711723eda7773eac889dea2f96ce99bb9 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 16:32:43 -0500 Subject: [PATCH 34/38] Clean up code coverage comment --- .github/workflows/pull-request-ci.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index baf2c1a..4e3fd3a 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -211,21 +211,20 @@ jobs: return "✅" with open(sys.argv[-1], "w") as handle: - print("### Combined Code Coverage", file=handle) + print("### Code Coverage", file=handle) print(file=handle) print("| Scope | Coverage | Status |", file=handle) - print("| --- | ---: | --- |", file=handle) + print("| --- | :---: | :---: |", file=handle) for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): print( f"| {scheme_name} | {coverage_percent:.2f}% | {status_emoji(coverage_percent)} |", file=handle, ) + indent = "          " + combined_percent = f"combined_coverage_percent:.2f" + combined_emoji = status_emoji(combined_coverage_percent) print( - "| **----------** | **----------:** | **---** |", - file=handle, - ) - print( - f"| **Combined** | **{combined_coverage_percent:.2f}%** | **{status_emoji(combined_coverage_percent)}** |", + f"| {indent} **Combined** | **{combined_percent}%** | **{combined_emoji}** |", file=handle, ) ' "${coverage_files[@]}" "$summary_file" From 5a1c470d147738aa3174601a92717ef2050ddb7e Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 16:43:19 -0500 Subject: [PATCH 35/38] Calculate coverage 1 time --- .github/workflows/pull-request-ci.yml | 70 ++++++++------------------- 1 file changed, 20 insertions(+), 50 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 4e3fd3a..85c5570 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -86,39 +86,6 @@ jobs: CODE_SIGNING_ALLOWED=NO \ test - - name: Export code coverage data - if: always() - env: - SCHEME: ${{ matrix.scheme }} - run: | - set -eo pipefail - - result_bundle="TestResults/${SCHEME}.xcresult" - coverage_json="TestResults/${SCHEME}.xccov.json" - - echo "### Code Coverage" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - - if [ ! -d "$result_bundle" ]; then - echo "Coverage report unavailable because \`$result_bundle\` was not created." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - if ! xcrun xccov view --archive --json "$result_bundle" > "$coverage_json"; then - echo "Coverage report unavailable because \`xccov\` could not parse \`$result_bundle\`." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - echo "Exported line-level coverage data for \`$SCHEME\`." >> "$GITHUB_STEP_SUMMARY" - - - name: Upload code coverage data - if: always() - uses: actions/upload-artifact@v6 - with: - name: unit-test-coverage-${{ matrix.scheme }} - path: TestResults/${{ matrix.scheme }}.xccov.json - if-no-files-found: ignore - - name: Upload unit test results if: always() uses: actions/upload-artifact@v6 @@ -128,33 +95,31 @@ jobs: coverage-summary: name: Combined Code Coverage - runs-on: ubuntu-latest + runs-on: macos-latest needs: unit-tests if: always() steps: - - name: Download code coverage data + - name: Download unit test results continue-on-error: true uses: actions/download-artifact@v7 with: - pattern: unit-test-coverage-* - path: CoverageData - merge-multiple: true + pattern: project-unit-tests-*.xcresult + path: CoverageResults - name: Publish combined coverage summary run: | set -eo pipefail - shopt -s nullglob + summary_file="CoverageResults/combined-coverage-summary.md" - mkdir -p CoverageData - coverage_files=(CoverageData/*.xccov.json) - summary_file="CoverageData/combined-coverage-summary.md" + mkdir -p CoverageResults + mapfile -d '' result_bundles < <(find CoverageResults -type d -name '*.xcresult' -print0 | sort -z) - if [ ${#coverage_files[@]} -eq 0 ]; then + if [ ${#result_bundles[@]} -eq 0 ]; then { echo "### Combined Code Coverage" echo - echo "Combined coverage unavailable because no coverage data artifacts were downloaded." + echo "Combined coverage unavailable because no unit test result bundles were downloaded." } > "$summary_file" cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" exit 0 @@ -163,15 +128,20 @@ jobs: python3 -c ' import json import os + import subprocess import sys combined = {} per_scheme = [] for path in sys.argv[1:-1]: - print(f"Processing coverage file: {path}", file=sys.stderr) - with open(path) as handle: - report = json.load(handle) + print(f"Processing result bundle: {path}", file=sys.stderr) + report = json.loads( + subprocess.check_output( + ["xcrun", "xccov", "view", "--archive", "--json", path], + text=True, + ) + ) covered_lines = 0 executable_lines = 0 @@ -189,7 +159,7 @@ jobs: combined_lines[line_number] = combined_lines.get(line_number, False) or is_covered - scheme_name = os.path.basename(path).replace(".xccov.json", "") + scheme_name = os.path.basename(path).replace(".xcresult", "") coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) @@ -221,7 +191,7 @@ jobs: file=handle, ) indent = "          " - combined_percent = f"combined_coverage_percent:.2f" + combined_percent = f"{combined_coverage_percent:.2f}" combined_emoji = status_emoji(combined_coverage_percent) print( f"| {indent} **Combined** | **{combined_percent}%** | **{combined_emoji}** |", @@ -240,7 +210,7 @@ jobs: run: | set -eo pipefail - summary_file="CoverageData/combined-coverage-summary.md" + summary_file="CoverageResults/combined-coverage-summary.md" if [ ! -f "$summary_file" ]; then echo "No combined coverage summary file was generated; skipping PR comment." exit 0 From 3cc7782672786a3cc09c564afb6093f47320d16c Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 16:54:19 -0500 Subject: [PATCH 36/38] "mapfile" not available --- .github/workflows/pull-request-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 85c5570..dc2f439 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -113,7 +113,10 @@ jobs: summary_file="CoverageResults/combined-coverage-summary.md" mkdir -p CoverageResults - mapfile -d '' result_bundles < <(find CoverageResults -type d -name '*.xcresult' -print0 | sort -z) + result_bundles=() + while IFS= read -r -d '' result_bundle; do + result_bundles+=("$result_bundle") + done < <(find CoverageResults -type d -name '*.xcresult' -print0) if [ ${#result_bundles[@]} -eq 0 ]; then { @@ -197,7 +200,7 @@ jobs: f"| {indent} **Combined** | **{combined_percent}%** | **{combined_emoji}** |", file=handle, ) - ' "${coverage_files[@]}" "$summary_file" + ' "${result_bundles[@]}" "$summary_file" cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" From beadc23fdb3543e971cdefa6b418452ccc1573c0 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 17:07:13 -0500 Subject: [PATCH 37/38] Print coverage results to the github action --- .github/workflows/pull-request-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index dc2f439..4adad2a 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -120,7 +120,7 @@ jobs: if [ ${#result_bundles[@]} -eq 0 ]; then { - echo "### Combined Code Coverage" + echo "### Code Coverage" echo echo "Combined coverage unavailable because no unit test result bundles were downloaded." } > "$summary_file" @@ -189,10 +189,12 @@ jobs: print("| Scope | Coverage | Status |", file=handle) print("| --- | :---: | :---: |", file=handle) for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): + emoji = status_emoji(coverage_percent) print( - f"| {scheme_name} | {coverage_percent:.2f}% | {status_emoji(coverage_percent)} |", + f"| {scheme_name} | {coverage_percent:.2f}% | {emoji} |", file=handle, ) + print(f"{scheme_name} - {coverage_percent:.2f}% {emoji}", file=sys.stderr) indent = "          " combined_percent = f"{combined_coverage_percent:.2f}" combined_emoji = status_emoji(combined_coverage_percent) @@ -200,6 +202,7 @@ jobs: f"| {indent} **Combined** | **{combined_percent}%** | **{combined_emoji}** |", file=handle, ) + print(f" Combined - {combined_percent}% {combined_emoji}", file=sys.stderr) ' "${result_bundles[@]}" "$summary_file" cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" From d241e622afb5458ac1d67797f6dc8570f8efedb7 Mon Sep 17 00:00:00 2001 From: Kris Kline Date: Wed, 8 Apr 2026 17:37:41 -0500 Subject: [PATCH 38/38] Update to also build main/master and add badges to the readme. Using ci-core.yml to centralize the logic to build and generate code coverage data --- .github/workflows/ci-core.yml | 309 ++++++++++++++++++++++++++ .github/workflows/main-ci.yml | 102 +++++++++ .github/workflows/pull-request-ci.yml | 289 +----------------------- README.md | 3 +- 4 files changed, 418 insertions(+), 285 deletions(-) create mode 100644 .github/workflows/ci-core.yml create mode 100644 .github/workflows/main-ci.yml diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml new file mode 100644 index 0000000..104d75a --- /dev/null +++ b/.github/workflows/ci-core.yml @@ -0,0 +1,309 @@ +name: CI Core + +on: + workflow_call: + inputs: + publish_pr_comment: + description: Publish the combined coverage summary as a PR comment. + required: false + default: false + type: boolean + outputs: + combined_coverage_percent: + description: Combined unit test coverage percent across all schemes. + value: ${{ jobs.coverage-summary.outputs.combined_coverage_percent }} + +jobs: + unit-tests: + name: Unit Tests (${{ matrix.scheme }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + scheme: + - libPhoneNumber + - libPhoneNumberGeocoding + - libPhoneNumberShortNumber + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Resolve iPhone simulator destination + id: destination + run: | + set -eo pipefail + + destination_id="$( + xcrun simctl list devices --json | + python3 -c ' + import json + import re + import sys + + matches = [] + for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): + runtime_match = re.match(r"com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)", runtime) + if not runtime_match: + continue + + print(f"Runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) + + os_version = tuple(int(part) for part in runtime_match.groups()) + for entry in entries: + name = (entry["name"] or "").strip() + model_match = re.match(r"iPhone\s+(\d+)\b", name) + udid = entry["udid"] + if entry["isAvailable"] and udid and model_match: + matches.append((os_version, int(model_match.group(1)), name, udid)) + the_os = f"{os_version[0]}.{os_version[1]}" + print(f" {name} (iOS {the_os}) [{udid}]", file=sys.stderr) + + if not matches: + raise SystemExit("No iPhone simulator destinations found") + + selected = max(matches); name = selected[2]; the_os = f"{selected[0][0]}.{selected[0][1]}" + print(f"Selected Simulator: {name} (iOS {the_os}) [{selected[3]}]", file=sys.stderr) + print(selected[3]) + ' + )" + + echo "destination=id=$destination_id,arch=arm64" >> "$GITHUB_OUTPUT" + + - name: Run unit tests + env: + SCHEME: ${{ matrix.scheme }} + run: | + set -eo pipefail + mkdir -p TestResults + + xcodebuild \ + -project libPhoneNumber.xcodeproj \ + -scheme "$SCHEME" \ + -destination "${{ steps.destination.outputs.destination }}" \ + -resultBundlePath "TestResults/${SCHEME}.xcresult" \ + -enableCodeCoverage YES \ + CODE_SIGNING_ALLOWED=NO \ + test + + - name: Upload unit test results + if: always() + uses: actions/upload-artifact@v6 + with: + name: project-unit-tests-${{ matrix.scheme }}.xcresult + path: TestResults/${{ matrix.scheme }}.xcresult + + coverage-summary: + name: Combined Code Coverage + runs-on: macos-latest + needs: unit-tests + if: always() + outputs: + combined_coverage_percent: ${{ steps.coverage.outputs.combined_coverage_percent }} + + steps: + - name: Download unit test results + continue-on-error: true + uses: actions/download-artifact@v7 + with: + pattern: project-unit-tests-*.xcresult + path: CoverageResults + + - name: Publish combined coverage summary + id: coverage + run: | + set -eo pipefail + summary_file="CoverageResults/combined-coverage-summary.md" + + mkdir -p CoverageResults + result_bundles=() + while IFS= read -r -d '' result_bundle; do + result_bundles+=("$result_bundle") + done < <(find CoverageResults -type d -name '*.xcresult' -print0) + + if [ ${#result_bundles[@]} -eq 0 ]; then + { + echo "### Combined Code Coverage" + echo + echo "Combined coverage unavailable because no unit test result bundles were downloaded." + } > "$summary_file" + echo "combined_coverage_percent=" >> "$GITHUB_OUTPUT" + cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + python3 -c ' + import json + import os + import subprocess + import sys + + combined = {} + per_scheme = [] + + for path in sys.argv[1:-1]: + print(f"Processing result bundle: {path}", file=sys.stderr) + report = json.loads( + subprocess.check_output( + ["xcrun", "xccov", "view", "--archive", "--json", path], + text=True, + ) + ) + + covered_lines = 0 + executable_lines = 0 + for file_path, entries in report.items(): + combined_lines = combined.setdefault(file_path, {}) + for entry in entries: + line_number = entry["line"] + if line_number is None or not entry["isExecutable"]: + continue + + is_covered = entry.get("executionCount", 0) > 0 + executable_lines += 1 + if is_covered: + covered_lines += 1 + + combined_lines[line_number] = combined_lines.get(line_number, False) or is_covered + + scheme_name = os.path.basename(path).replace(".xcresult", "") + coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 + per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) + + combined_executable_lines = sum(len(lines) for lines in combined.values()) + combined_covered_lines = sum( + 1 for lines in combined.values() for is_covered in lines.values() if is_covered + ) + combined_coverage_percent = ( + combined_covered_lines / combined_executable_lines * 100 + if combined_executable_lines + else 0.0 + ) + + def status_emoji(coverage_percent): + if coverage_percent < 60.0: + return "❌" + if coverage_percent < 75.0: + return "⚠️" + return "✅" + + with open(sys.argv[-1], "w") as handle: + print("### Code Coverage", file=handle) + print(file=handle) + print("| Scope | Coverage | Status |", file=handle) + print("| --- | :---: | :---: |", file=handle) + for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): + emoji = status_emoji(coverage_percent) + print( + f"| {scheme_name} | {coverage_percent:.2f}% | {emoji} |", + file=handle, + ) + print(f"{scheme_name} - {coverage_percent:.2f}% {emoji}", file=sys.stderr) + indent = "          " + combined_percent = f"{combined_coverage_percent:.2f}" + combined_emoji = status_emoji(combined_coverage_percent) + print( + f"| {indent} **Combined** | **{combined_percent}%** | **{combined_emoji}** |", + file=handle, + ) + print(f" Combined - {combined_percent}% {combined_emoji}", file=sys.stderr) + + with open(os.environ["GITHUB_OUTPUT"], "a") as handle: + print(f"combined_coverage_percent={combined_coverage_percent:.2f}", file=handle) + ' "${result_bundles[@]}" "$summary_file" + + echo "Combined coverage summary:" + cat "$summary_file" + cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" + + - name: Publish combined coverage comment to pull request + if: inputs.publish_pr_comment + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + run: | + set -eo pipefail + + summary_file="CoverageResults/combined-coverage-summary.md" + if [ ! -f "$summary_file" ]; then + echo "No combined coverage summary file was generated; skipping PR comment." + exit 0 + fi + + comment_id="$( + curl -fsSL \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | + python3 -c ' + import json + import sys + + marker = "" + comments = json.load(sys.stdin) + + for comment in comments: + if marker in comment.get("body", ""): + print(comment["id"]) + break + ' + )" + + payload="$( + python3 -c ' + import json + import sys + + with open(sys.argv[1]) as handle: + body = "\n\n" + handle.read() + + print(json.dumps({"body": body})) + ' "$summary_file" + )" + + if [ -n "$comment_id" ]; then + echo "Updating existing coverage PR comment: $comment_id" + curl -fsSL \ + -X PATCH \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + --data "$payload" \ + "https://api.github.com/repos/$REPOSITORY/issues/comments/$comment_id" \ + > /dev/null + else + echo "Creating new coverage PR comment" + curl -fsSL \ + -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + --data "$payload" \ + "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ + > /dev/null + fi + + podspec-lint: + name: Podspec Lint (${{ matrix.podspec }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + podspec: + - libPhoneNumber-iOS.podspec + - libPhoneNumberGeocoding.podspec + - libPhoneNumberShortNumber.podspec + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Ensure CocoaPods is installed + run: | + if ! command -v pod >/dev/null; then + gem install cocoapods + fi + + - name: Lint podspec + run: pod lib lint "${{ matrix.podspec }}" --verbose diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml new file mode 100644 index 0000000..1a95f29 --- /dev/null +++ b/.github/workflows/main-ci.yml @@ -0,0 +1,102 @@ +name: Main CI + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: main-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + uses: ./.github/workflows/ci-core.yml + with: + publish_pr_comment: false + secrets: inherit + + build-coverage-badge: + name: Build Coverage Badge + runs-on: ubuntu-latest + needs: ci + if: ${{ needs.ci.result == 'success' }} + + steps: + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Build badge site + env: + COMBINED_COVERAGE_PERCENT: ${{ needs.ci.outputs.combined_coverage_percent }} + run: | + set -eo pipefail + mkdir -p badge-site/badges + touch badge-site/.nojekyll + + python3 -c ' + import json + import os + + coverage_text = os.environ.get("COMBINED_COVERAGE_PERCENT", "").strip() + if coverage_text: + coverage_percent = float(coverage_text) + if coverage_percent < 60.0: + color = "red" + elif coverage_percent < 75.0: + color = "yellow" + else: + color = "brightgreen" + message = f"{coverage_percent:.2f}%" + else: + color = "lightgrey" + message = "n/a" + + payload = { + "schemaVersion": 1, + "label": "coverage", + "message": message, + "color": color, + } + + with open("badge-site/badges/coverage.json", "w") as handle: + json.dump(payload, handle) + ' + + cat > badge-site/index.html <<'EOF' + + + + + libPhoneNumber-iOS Badges + + +

Badge assets for libPhoneNumber-iOS.

+ + + EOF + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: badge-site + + deploy-coverage-badge: + name: Deploy Coverage Badge + runs-on: ubuntu-latest + needs: build-coverage-badge + if: ${{ needs.build-coverage-badge.result == 'success' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 4adad2a..fb80329 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -14,287 +14,8 @@ concurrency: cancel-in-progress: true jobs: - unit-tests: - name: Unit Tests (${{ matrix.scheme }}) - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - scheme: - - libPhoneNumber - - libPhoneNumberGeocoding - - libPhoneNumberShortNumber - - steps: - - name: Check out repository - uses: actions/checkout@v6 - - - name: Resolve iPhone simulator destination - id: destination - run: | - set -eo pipefail - - destination_id="$( - xcrun simctl list devices --json | - python3 -c ' - import json - import re - import sys - - matches = [] - for runtime, entries in json.load(sys.stdin).get("devices", {}).items(): - runtime_match = re.match(r"com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)", runtime) - if not runtime_match: - continue - - print(f"Runtime: {runtime} ({len(entries)} devices)", file=sys.stderr) - - os_version = tuple(int(part) for part in runtime_match.groups()) - for entry in entries: - name = (entry["name"] or "").strip() - model_match = re.match(r"iPhone\s+(\d+)\b", name) - udid = entry["udid"] - if entry["isAvailable"] and udid and model_match: - matches.append((os_version, int(model_match.group(1)), name, udid)) - the_os = f"{os_version[0]}.{os_version[1]}" - print(f" {name} (iOS {the_os}) [{udid}]", file=sys.stderr) - - if not matches: - raise SystemExit("No iPhone simulator destinations found") - - selected = max(matches); name = selected[2]; the_os = f"{selected[0][0]}.{selected[0][1]}" - print(f"Selected Simulator: {name} (iOS {the_os}) [{selected[3]}]", file=sys.stderr) - print(selected[3]) - ' - )" - - echo "destination=id=$destination_id,arch=arm64" >> "$GITHUB_OUTPUT" - - - name: Run unit tests - env: - SCHEME: ${{ matrix.scheme }} - run: | - set -eo pipefail - mkdir -p TestResults - - xcodebuild \ - -project libPhoneNumber.xcodeproj \ - -scheme "$SCHEME" \ - -destination "${{ steps.destination.outputs.destination }}" \ - -resultBundlePath "TestResults/${SCHEME}.xcresult" \ - -enableCodeCoverage YES \ - CODE_SIGNING_ALLOWED=NO \ - test - - - name: Upload unit test results - if: always() - uses: actions/upload-artifact@v6 - with: - name: project-unit-tests-${{ matrix.scheme }}.xcresult - path: TestResults/${{ matrix.scheme }}.xcresult - - coverage-summary: - name: Combined Code Coverage - runs-on: macos-latest - needs: unit-tests - if: always() - - steps: - - name: Download unit test results - continue-on-error: true - uses: actions/download-artifact@v7 - with: - pattern: project-unit-tests-*.xcresult - path: CoverageResults - - - name: Publish combined coverage summary - run: | - set -eo pipefail - summary_file="CoverageResults/combined-coverage-summary.md" - - mkdir -p CoverageResults - result_bundles=() - while IFS= read -r -d '' result_bundle; do - result_bundles+=("$result_bundle") - done < <(find CoverageResults -type d -name '*.xcresult' -print0) - - if [ ${#result_bundles[@]} -eq 0 ]; then - { - echo "### Code Coverage" - echo - echo "Combined coverage unavailable because no unit test result bundles were downloaded." - } > "$summary_file" - cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - python3 -c ' - import json - import os - import subprocess - import sys - - combined = {} - per_scheme = [] - - for path in sys.argv[1:-1]: - print(f"Processing result bundle: {path}", file=sys.stderr) - report = json.loads( - subprocess.check_output( - ["xcrun", "xccov", "view", "--archive", "--json", path], - text=True, - ) - ) - - covered_lines = 0 - executable_lines = 0 - for file_path, entries in report.items(): - combined_lines = combined.setdefault(file_path, {}) - for entry in entries: - line_number = entry["line"] - if line_number is None or not entry["isExecutable"]: - continue - - is_covered = entry.get("executionCount", 0) > 0 - executable_lines += 1 - if is_covered: - covered_lines += 1 - - combined_lines[line_number] = combined_lines.get(line_number, False) or is_covered - - scheme_name = os.path.basename(path).replace(".xcresult", "") - coverage_percent = (covered_lines / executable_lines * 100) if executable_lines else 0.0 - per_scheme.append((scheme_name, coverage_percent, covered_lines, executable_lines)) - - combined_executable_lines = sum(len(lines) for lines in combined.values()) - combined_covered_lines = sum( - 1 for lines in combined.values() for is_covered in lines.values() if is_covered - ) - combined_coverage_percent = ( - combined_covered_lines / combined_executable_lines * 100 - if combined_executable_lines - else 0.0 - ) - - def status_emoji(coverage_percent): - if coverage_percent < 60.0: - return "❌" - if coverage_percent < 75.0: - return "⚠️" - return "✅" - - with open(sys.argv[-1], "w") as handle: - print("### Code Coverage", file=handle) - print(file=handle) - print("| Scope | Coverage | Status |", file=handle) - print("| --- | :---: | :---: |", file=handle) - for scheme_name, coverage_percent, covered_lines, executable_lines in sorted(per_scheme): - emoji = status_emoji(coverage_percent) - print( - f"| {scheme_name} | {coverage_percent:.2f}% | {emoji} |", - file=handle, - ) - print(f"{scheme_name} - {coverage_percent:.2f}% {emoji}", file=sys.stderr) - indent = "          " - combined_percent = f"{combined_coverage_percent:.2f}" - combined_emoji = status_emoji(combined_coverage_percent) - print( - f"| {indent} **Combined** | **{combined_percent}%** | **{combined_emoji}** |", - file=handle, - ) - print(f" Combined - {combined_percent}% {combined_emoji}", file=sys.stderr) - ' "${result_bundles[@]}" "$summary_file" - - cat "$summary_file" >> "$GITHUB_STEP_SUMMARY" - - - name: Publish combined coverage comment to pull request - if: github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPOSITORY: ${{ github.repository }} - run: | - set -eo pipefail - - summary_file="CoverageResults/combined-coverage-summary.md" - if [ ! -f "$summary_file" ]; then - echo "No combined coverage summary file was generated; skipping PR comment." - exit 0 - fi - - comment_id="$( - curl -fsSL \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" | - python3 -c ' - import json - import sys - - marker = "" - comments = json.load(sys.stdin) - - for comment in comments: - if marker in comment.get("body", ""): - print(comment["id"]) - break - ' - )" - - payload="$( - python3 -c ' - import json - import sys - - with open(sys.argv[1]) as handle: - body = "\n\n" + handle.read() - - print(json.dumps({"body": body})) - ' "$summary_file" - )" - - if [ -n "$comment_id" ]; then - echo "Updating existing coverage PR comment: $comment_id" - curl -fsSL \ - -X PATCH \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "Content-Type: application/json" \ - --data "$payload" \ - "https://api.github.com/repos/$REPOSITORY/issues/comments/$comment_id" \ - > /dev/null - else - echo "Creating new coverage PR comment" - curl -fsSL \ - -X POST \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "Content-Type: application/json" \ - --data "$payload" \ - "https://api.github.com/repos/$REPOSITORY/issues/$PR_NUMBER/comments" \ - > /dev/null - fi - - podspec-lint: - name: Podspec Lint (${{ matrix.podspec }}) - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - podspec: - - libPhoneNumber-iOS.podspec - - libPhoneNumberGeocoding.podspec - - libPhoneNumberShortNumber.podspec - - steps: - - name: Check out repository - uses: actions/checkout@v6 - - - name: Ensure CocoaPods is installed - run: | - if ! command -v pod >/dev/null; then - gem install cocoapods - fi - - - name: Lint podspec - run: pod lib lint "${{ matrix.podspec }}" --verbose + ci: + uses: ./.github/workflows/ci-core.yml + with: + publish_pr_comment: true + secrets: inherit diff --git a/README.md b/README.md index 5e3d5fb..42093ad 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![CocoaPods](https://img.shields.io/cocoapods/p/libPhoneNumber-iOS.svg?style=flat)](http://cocoapods.org/?q=libPhoneNumber-iOS) [![CocoaPods](https://img.shields.io/cocoapods/v/libPhoneNumber-iOS.svg?style=flat)](http://cocoapods.org/?q=libPhoneNumber-iOS) -[![Pull Request CI](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/pull-request-ci.yml/badge.svg?branch=master)](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/pull-request-ci.yml) +[![Main CI](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/main-ci.yml/badge.svg?branch=master)](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/main-ci.yml) +[![Coverage](https://img.shields.io/endpoint?url=https://iziz.github.io/libPhoneNumber-iOS/badges/coverage.json)](https://github.com/iziz/libPhoneNumber-iOS/actions/workflows/main-ci.yml) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) # **libPhoneNumber for iOS**