Commit 104dd73fbc

104dd73fbc82a9d57eb4681ec7f7b43313d24e67

parent: ebe0e42d13

Unsigned

cmc <hello@cleberg.net> · 2026-09-07 21:36 UTC

Deployment target 18.0 (!84)

The app compiled clean at 18.0 with no code change; 26.5 was the new-project default. Both packages accept iOS 16.

Verified on a downloaded iOS 18.6 runtime (iPhone 16) and on 26.5: unit suite green on both; all 14 live smoke tests green on both. Two things surfaced only on 18: tests that built their own decoder with .iso8601 failed on fractional seconds, so they now use GitbayClient.decoder(); and list section headers render uppercased, so the suite matches them ignoring case. The suite also signs in at setup when a device starts on the token screen.

Layout: unified · split

README.org +1 −1
@@ -46,7 +46,7 @@ Any gitbay instance works. You just need to enter the host at sign-in.
46 46
47* Building 47* Building
48 48
49Requires Xcode with an iOS 26.5 SDK. Swift 6 language mode, default =MainActor= 49Builds with the iOS 26.5 SDK and runs on iOS 18.0 and later. Swift 6 language mode, default =MainActor=
50isolation. 50isolation.
51 51
52#+begin_src sh 52#+begin_src sh
gitbay.xcodeproj/project.pbxproj +4 −2
@@ -338,7 +338,7 @@
338 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 338 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
339 GCC_WARN_UNUSED_FUNCTION = YES; 339 GCC_WARN_UNUSED_FUNCTION = YES;
340 GCC_WARN_UNUSED_VARIABLE = YES; 340 GCC_WARN_UNUSED_VARIABLE = YES;
341 IPHONEOS_DEPLOYMENT_TARGET = 26.5; 341 IPHONEOS_DEPLOYMENT_TARGET = 18.0;
342 LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 342 LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
343 MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 343 MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
344 MTL_FAST_MATH = YES; 344 MTL_FAST_MATH = YES;
@@ -396,7 +396,7 @@
396 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
397 GCC_WARN_UNUSED_FUNCTION = YES; 397 GCC_WARN_UNUSED_FUNCTION = YES;
398 GCC_WARN_UNUSED_VARIABLE = YES; 398 GCC_WARN_UNUSED_VARIABLE = YES;
399 IPHONEOS_DEPLOYMENT_TARGET = 26.5; 399 IPHONEOS_DEPLOYMENT_TARGET = 18.0;
400 LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 400 LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
401 MTL_ENABLE_DEBUG_INFO = NO; 401 MTL_ENABLE_DEBUG_INFO = NO;
402 MTL_FAST_MATH = YES; 402 MTL_FAST_MATH = YES;
@@ -416,6 +416,7 @@
416 DEVELOPMENT_TEAM = ZCNAX3VL9D; 416 DEVELOPMENT_TEAM = ZCNAX3VL9D;
417 ENABLE_PREVIEWS = YES; 417 ENABLE_PREVIEWS = YES;
418 GENERATE_INFOPLIST_FILE = YES; 418 GENERATE_INFOPLIST_FILE = YES;
419 INFOPLIST_KEY_CFBundleDisplayName = gitbay;
419 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 420 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
420 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 421 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
421 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 422 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -450,6 +451,7 @@
450 DEVELOPMENT_TEAM = ZCNAX3VL9D; 451 DEVELOPMENT_TEAM = ZCNAX3VL9D;
451 ENABLE_PREVIEWS = YES; 452 ENABLE_PREVIEWS = YES;
452 GENERATE_INFOPLIST_FILE = YES; 453 GENERATE_INFOPLIST_FILE = YES;
454 INFOPLIST_KEY_CFBundleDisplayName = gitbay;
453 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 455 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
454 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 456 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
455 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 457 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
gitbay/Networking/GitbayClient.swift +4 −1
@@ -255,7 +255,10 @@ nonisolated final class GitbayClient: Sendable {
255 255
256 /// Dates on the wire are RFC 3339, some with fractional seconds 256 /// Dates on the wire are RFC 3339, some with fractional seconds
257 /// (SQLite's `%Y-%m-%dT%H:%M:%fZ` default), some without (`repo log`). 257 /// (SQLite's `%Y-%m-%dT%H:%M:%fZ` default), some without (`repo log`).
258 private static func decoder() -> JSONDecoder { 258 /// Internal so tests decode fixtures the way the client does: the
259 /// stock `.iso8601` strategy accepts fractional seconds on iOS 26 and
260 /// refuses them on iOS 18.
261 static func decoder() -> JSONDecoder {
259 let decoder = JSONDecoder() 262 let decoder = JSONDecoder()
260 decoder.dateDecodingStrategy = .custom { decoder in 263 decoder.dateDecodingStrategy = .custom { decoder in
261 let text = try decoder.singleValueContainer().decode(String.self) 264 let text = try decoder.singleValueContainer().decode(String.self)
gitbayTests/BodyFormatTests.swift +2 −4
@@ -25,14 +25,12 @@ private func stdinOf(_ seen: StubProtocol.Seen) throws -> String? {
25struct BodyFormatDecodingTests { 25struct BodyFormatDecodingTests {
26 26
27 private func decodeIssue(_ json: String) throws -> IssueDetail { 27 private func decodeIssue(_ json: String) throws -> IssueDetail {
28 let decoder = JSONDecoder() 28 let decoder = GitbayClient.decoder()
29 decoder.dateDecodingStrategy = .iso8601
30 return try decoder.decode(IssueDetail.self, from: Data(json.utf8)) 29 return try decoder.decode(IssueDetail.self, from: Data(json.utf8))
31 } 30 }
32 31
33 private func decodeMR(_ json: String) throws -> MRDetail { 32 private func decodeMR(_ json: String) throws -> MRDetail {
34 let decoder = JSONDecoder() 33 let decoder = GitbayClient.decoder()
35 decoder.dateDecodingStrategy = .iso8601
36 return try decoder.decode(MRDetail.self, from: Data(json.utf8)) 34 return try decoder.decode(MRDetail.self, from: Data(json.utf8))
37 } 35 }
38 36
gitbayTests/DepsTests.swift +1 −2
@@ -33,8 +33,7 @@ private let fullStatus = """
33struct DepsDecodingTests { 33struct DepsDecodingTests {
34 34
35 private func decode(_ json: String) throws -> DepsStatus { 35 private func decode(_ json: String) throws -> DepsStatus {
36 let decoder = JSONDecoder() 36 let decoder = GitbayClient.decoder()
37 decoder.dateDecodingStrategy = .iso8601
38 return try decoder.decode(DepsStatus.self, from: Data(json.utf8)) 37 return try decoder.decode(DepsStatus.self, from: Data(json.utf8))
39 } 38 }
40 39
gitbayTests/DiscoveryTests.swift +1 −2
@@ -15,8 +15,7 @@ private func makeClient() throws -> (GitbayClient, StubProtocol.Box) {
15struct FeedEventTests { 15struct FeedEventTests {
16 16
17 private func decode(_ json: String) throws -> FeedEvent { 17 private func decode(_ json: String) throws -> FeedEvent {
18 let decoder = JSONDecoder() 18 let decoder = GitbayClient.decoder()
19 decoder.dateDecodingStrategy = .iso8601
20 return try decoder.decode(FeedEvent.self, from: Data(json.utf8)) 19 return try decoder.decode(FeedEvent.self, from: Data(json.utf8))
21 } 20 }
22 21
gitbayTests/MRLifecycleTests.swift +2 −4
@@ -3,14 +3,12 @@ import Testing
3@testable import gitbay 3@testable import gitbay
4 4
5private func decodeDetail(_ json: String) throws -> MRDetail { 5private func decodeDetail(_ json: String) throws -> MRDetail {
6 let decoder = JSONDecoder() 6 let decoder = GitbayClient.decoder()
7 decoder.dateDecodingStrategy = .iso8601
8 return try decoder.decode(MRDetail.self, from: Data(json.utf8)) 7 return try decoder.decode(MRDetail.self, from: Data(json.utf8))
9} 8}
10 9
11private func decodeRow(_ json: String) throws -> MergeRequest { 10private func decodeRow(_ json: String) throws -> MergeRequest {
12 let decoder = JSONDecoder() 11 let decoder = GitbayClient.decoder()
13 decoder.dateDecodingStrategy = .iso8601
14 return try decoder.decode(MergeRequest.self, from: Data(json.utf8)) 12 return try decoder.decode(MergeRequest.self, from: Data(json.utf8))
15} 13}
16 14
gitbayTests/MRViewModelTests.swift +1 −2
@@ -384,8 +384,7 @@ struct ThreadAnchoringTests {
384 "comments":[{"id":1,"author":"krz","body":"why?",\ 384 "comments":[{"id":1,"author":"krz","body":"why?",\
385 "created_at":"2026-08-20T10:30:00.000Z"}]} 385 "created_at":"2026-08-20T10:30:00.000Z"}]}
386 """ 386 """
387 let decoder = JSONDecoder() 387 let decoder = GitbayClient.decoder()
388 decoder.dateDecodingStrategy = .iso8601
389 return try decoder.decode(ReviewThread.self, from: Data(json.utf8)) 388 return try decoder.decode(ReviewThread.self, from: Data(json.utf8))
390 } 389 }
391 390
gitbayTests/NotificationTests.swift +1 −2
@@ -23,8 +23,7 @@ private func argvOf(_ seen: StubProtocol.Seen) throws -> [String] {
23} 23}
24 24
25private func decode(_ json: String) throws -> InboxNotification { 25private func decode(_ json: String) throws -> InboxNotification {
26 let decoder = JSONDecoder() 26 let decoder = GitbayClient.decoder()
27 decoder.dateDecodingStrategy = .iso8601
28 return try decoder.decode(InboxNotification.self, from: Data(json.utf8)) 27 return try decoder.decode(InboxNotification.self, from: Data(json.utf8))
29} 28}
30 29
gitbayTests/ReleaseTests.swift +1 −2
@@ -161,8 +161,7 @@ struct ReleaseDetailViewModelTests {
161struct FeedReleaseDestinationTests { 161struct FeedReleaseDestinationTests {
162 162
163 @Test func releaseEventsNavigateToTheRelease() throws { 163 @Test func releaseEventsNavigateToTheRelease() throws {
164 let decoder = JSONDecoder() 164 let decoder = GitbayClient.decoder()
165 decoder.dateDecodingStrategy = .iso8601
166 let event = try decoder.decode(FeedEvent.self, from: Data(""" 165 let event = try decoder.decode(FeedEvent.self, from: Data("""
167 {"id":2,"repo":"krz/gitbay","actor":"cmc","kind":"release.created",\ 166 {"id":2,"repo":"krz/gitbay","actor":"cmc","kind":"release.created",\
168 "data":{"tag":"v1.0.0"},"created_at":"2026-08-26T05:27:36Z"} 167 "data":{"tag":"v1.0.0"},"created_at":"2026-08-26T05:27:36Z"}
gitbayTests/SearchTests.swift +2 −4
@@ -29,11 +29,9 @@ private let mixedResults = """
29 29
30struct SearchResultDecodingTests { 30struct SearchResultDecodingTests {
31 31
32 /// Matches the established direct-decode pattern in 32 /// Decodes with the client's own date strategy, as `DiscoveryTests` does.
33 /// `DiscoveryTests.swift`: a plain decoder with `.iso8601` dates.
34 private func decode(_ json: String) throws -> [SearchResult] { 33 private func decode(_ json: String) throws -> [SearchResult] {
35 let decoder = JSONDecoder() 34 let decoder = GitbayClient.decoder()
36 decoder.dateDecodingStrategy = .iso8601
37 return try decoder.decode([SearchResult].self, from: Data(json.utf8)) 35 return try decoder.decode([SearchResult].self, from: Data(json.utf8))
38 } 36 }
39 37
gitbayUITests/LiveSmokeUITests.swift +19 −5
@@ -39,6 +39,9 @@ final class LiveSmokeUITests: XCTestCase {
39 default: break // follow the device, as a real launch does 39 default: break // follow the device, as a real launch does
40 } 40 }
41 app.launch() 41 app.launch()
42 // A fresh device — a clone on another runtime, say — starts on
43 // the token screen; the flows assume a signed-in app.
44 signInIfNeeded()
42 if name.contains("testRepoManagementFlows") { 45 if name.contains("testRepoManagementFlows") {
43 deleteScratchRepo() 46 deleteScratchRepo()
44 } 47 }
@@ -242,7 +245,7 @@ final class LiveSmokeUITests: XCTestCase {
242 createdRow.tap() 245 createdRow.tap()
243 246
244 // A fresh MR has one revision; the section lists it. 247 // A fresh MR has one revision; the section lists it.
245 let revisions = app.staticTexts["Revisions"].firstMatch 248 let revisions = header("Revisions")
246 XCTAssertTrue(scrollTo(revisions, swipes: 8), "revisions section missing") 249 XCTAssertTrue(scrollTo(revisions, swipes: 8), "revisions section missing")
247 XCTAssertTrue(app.staticTexts["v1"].firstMatch.exists, "first revision not listed") 250 XCTAssertTrue(app.staticTexts["v1"].firstMatch.exists, "first revision not listed")
248 251
@@ -320,6 +323,12 @@ final class LiveSmokeUITests: XCTestCase {
320 /// rows do not exist to XCUITest, so waitForExistence alone fails on 323 /// rows do not exist to XCUITest, so waitForExistence alone fails on
321 /// anything below the fold. 324 /// anything below the fold.
322 @discardableResult 325 @discardableResult
326 /// A list section header. iOS 18 renders these uppercased and iOS 26
327 /// as written, so the match ignores case.
328 func header(_ text: String) -> XCUIElement {
329 app.staticTexts.matching(NSPredicate(format: "label ==[c] %@", text)).firstMatch
330 }
331
323 func scrollTo(_ element: XCUIElement, swipes: Int = 6) -> Bool { 332 func scrollTo(_ element: XCUIElement, swipes: Int = 6) -> Bool {
324 for _ in 0..<swipes { 333 for _ in 0..<swipes {
325 if element.exists { return true } 334 if element.exists { return true }
@@ -650,7 +659,7 @@ extension LiveSmokeUITests {
650 XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch 659 XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch
651 .waitForExistence(timeout: 15), "profile did not load") 660 .waitForExistence(timeout: 15), "profile did not load")
652 XCTAssertTrue(app.staticTexts 661 XCTAssertTrue(app.staticTexts
653 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch 662 .containing(NSPredicate(format: "label BEGINSWITH[c] 'Repositories'")).firstMatch
654 .waitForExistence(timeout: 10), "profile repos missing") 663 .waitForExistence(timeout: 10), "profile repos missing")
655 back() 664 back()
656 665
@@ -862,6 +871,11 @@ extension LiveSmokeUITests {
862 } 871 }
863 872
864 private func signInIfNeeded() { 873 private func signInIfNeeded() {
874 // Signed in already: the tab bar is up and there is no token field.
875 if app.tabBars.firstMatch.waitForExistence(timeout: 10),
876 !app.secureTextFields.firstMatch.exists {
877 return
878 }
865 let tokenField = app.secureTextFields.firstMatch 879 let tokenField = app.secureTextFields.firstMatch
866 guard tokenField.waitForExistence(timeout: 5) else { return } 880 guard tokenField.waitForExistence(timeout: 5) else { return }
867 guard let token = ProcessInfo.processInfo.environment["GITBAY_UITEST_TOKEN"] else { 881 guard let token = ProcessInfo.processInfo.environment["GITBAY_UITEST_TOKEN"] else {
@@ -1010,7 +1024,7 @@ extension LiveSmokeUITests {
1010 XCTAssertTrue(app.staticTexts 1024 XCTAssertTrue(app.staticTexts
1011 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch 1025 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch
1012 .waitForExistence(timeout: 20), "profile description missing") 1026 .waitForExistence(timeout: 20), "profile description missing")
1013 XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists, 1027 XCTAssertTrue(header("Organizations").exists,
1014 "org memberships missing") 1028 "org memberships missing")
1015 XCTAssertTrue(app.staticTexts["krz"].firstMatch.exists, "org row missing") 1029 XCTAssertTrue(app.staticTexts["krz"].firstMatch.exists, "org row missing")
1016 1030
@@ -1027,7 +1041,7 @@ extension LiveSmokeUITests {
1027 .containing(NSPredicate(format: "label CONTAINS 'contributions in the last year'")).firstMatch), 1041 .containing(NSPredicate(format: "label CONTAINS 'contributions in the last year'")).firstMatch),
1028 "activity graph missing") 1042 "activity graph missing")
1029 XCTAssertTrue(scrollTo(app.staticTexts 1043 XCTAssertTrue(scrollTo(app.staticTexts
1030 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch), 1044 .containing(NSPredicate(format: "label BEGINSWITH[c] 'Repositories'")).firstMatch),
1031 "repositories missing") 1045 "repositories missing")
1032 1046
1033 // --- a log entry opens its commit --- 1047 // --- a log entry opens its commit ---
@@ -1063,7 +1077,7 @@ extension LiveSmokeUITests {
1063 XCTAssertTrue(main.waitForExistence(timeout: 20), "refs did not load") 1077 XCTAssertTrue(main.waitForExistence(timeout: 20), "refs did not load")
1064 XCTAssertTrue(app.staticTexts["default"].firstMatch.exists, 1078 XCTAssertTrue(app.staticTexts["default"].firstMatch.exists,
1065 "the default branch is not marked") 1079 "the default branch is not marked")
1066 XCTAssertTrue(app.staticTexts["Tags"].firstMatch.exists, "tags section missing") 1080 XCTAssertTrue(header("Tags").exists, "tags section missing")
1067 main.tap() 1081 main.tap()
1068 // Tapping a ref browses the repository there. 1082 // Tapping a ref browses the repository there.
1069 XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), 1083 XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20),