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.
4646
4747* Building
4848
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=
5050isolation.
5151
5252#+begin_src sh
gitbay.xcodeproj/project.pbxproj +4 −2
@@ -338,7 +338,7 @@
338338 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
339339 GCC_WARN_UNUSED_FUNCTION = YES;
340340 GCC_WARN_UNUSED_VARIABLE = YES;
341 IPHONEOS_DEPLOYMENT_TARGET = 26.5;
341 IPHONEOS_DEPLOYMENT_TARGET = 18.0;
342342 LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
343343 MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
344344 MTL_FAST_MATH = YES;
@@ -396,7 +396,7 @@
396396 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
397397 GCC_WARN_UNUSED_FUNCTION = YES;
398398 GCC_WARN_UNUSED_VARIABLE = YES;
399 IPHONEOS_DEPLOYMENT_TARGET = 26.5;
399 IPHONEOS_DEPLOYMENT_TARGET = 18.0;
400400 LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
401401 MTL_ENABLE_DEBUG_INFO = NO;
402402 MTL_FAST_MATH = YES;
@@ -416,6 +416,7 @@
416416 DEVELOPMENT_TEAM = ZCNAX3VL9D;
417417 ENABLE_PREVIEWS = YES;
418418 GENERATE_INFOPLIST_FILE = YES;
419 INFOPLIST_KEY_CFBundleDisplayName = gitbay;
419420 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
420421 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
421422 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -450,6 +451,7 @@
450451 DEVELOPMENT_TEAM = ZCNAX3VL9D;
451452 ENABLE_PREVIEWS = YES;
452453 GENERATE_INFOPLIST_FILE = YES;
454 INFOPLIST_KEY_CFBundleDisplayName = gitbay;
453455 INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
454456 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
455457 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
gitbay/Networking/GitbayClient.swift +4 −1
@@ -255,7 +255,10 @@ nonisolated final class GitbayClient: Sendable {
255255
256256 /// Dates on the wire are RFC 3339, some with fractional seconds
257257 /// (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 {
259262 let decoder = JSONDecoder()
260263 decoder.dateDecodingStrategy = .custom { decoder in
261264 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? {
2525struct BodyFormatDecodingTests {
2626
2727 private func decodeIssue(_ json: String) throws -> IssueDetail {
28 let decoder = JSONDecoder()
29 decoder.dateDecodingStrategy = .iso8601
28 let decoder = GitbayClient.decoder()
3029 return try decoder.decode(IssueDetail.self, from: Data(json.utf8))
3130 }
3231
3332 private func decodeMR(_ json: String) throws -> MRDetail {
34 let decoder = JSONDecoder()
35 decoder.dateDecodingStrategy = .iso8601
33 let decoder = GitbayClient.decoder()
3634 return try decoder.decode(MRDetail.self, from: Data(json.utf8))
3735 }
3836
gitbayTests/DepsTests.swift +1 −2
@@ -33,8 +33,7 @@ private let fullStatus = """
3333struct DepsDecodingTests {
3434
3535 private func decode(_ json: String) throws -> DepsStatus {
36 let decoder = JSONDecoder()
37 decoder.dateDecodingStrategy = .iso8601
36 let decoder = GitbayClient.decoder()
3837 return try decoder.decode(DepsStatus.self, from: Data(json.utf8))
3938 }
4039
gitbayTests/DiscoveryTests.swift +1 −2
@@ -15,8 +15,7 @@ private func makeClient() throws -> (GitbayClient, StubProtocol.Box) {
1515struct FeedEventTests {
1616
1717 private func decode(_ json: String) throws -> FeedEvent {
18 let decoder = JSONDecoder()
19 decoder.dateDecodingStrategy = .iso8601
18 let decoder = GitbayClient.decoder()
2019 return try decoder.decode(FeedEvent.self, from: Data(json.utf8))
2120 }
2221
gitbayTests/MRLifecycleTests.swift +2 −4
@@ -3,14 +3,12 @@ import Testing
33@testable import gitbay
44
55private func decodeDetail(_ json: String) throws -> MRDetail {
6 let decoder = JSONDecoder()
7 decoder.dateDecodingStrategy = .iso8601
6 let decoder = GitbayClient.decoder()
87 return try decoder.decode(MRDetail.self, from: Data(json.utf8))
98}
109
1110private func decodeRow(_ json: String) throws -> MergeRequest {
12 let decoder = JSONDecoder()
13 decoder.dateDecodingStrategy = .iso8601
11 let decoder = GitbayClient.decoder()
1412 return try decoder.decode(MergeRequest.self, from: Data(json.utf8))
1513}
1614
gitbayTests/MRViewModelTests.swift +1 −2
@@ -384,8 +384,7 @@ struct ThreadAnchoringTests {
384384 "comments":[{"id":1,"author":"krz","body":"why?",\
385385 "created_at":"2026-08-20T10:30:00.000Z"}]}
386386 """
387 let decoder = JSONDecoder()
388 decoder.dateDecodingStrategy = .iso8601
387 let decoder = GitbayClient.decoder()
389388 return try decoder.decode(ReviewThread.self, from: Data(json.utf8))
390389 }
391390
gitbayTests/NotificationTests.swift +1 −2
@@ -23,8 +23,7 @@ private func argvOf(_ seen: StubProtocol.Seen) throws -> [String] {
2323}
2424
2525private func decode(_ json: String) throws -> InboxNotification {
26 let decoder = JSONDecoder()
27 decoder.dateDecodingStrategy = .iso8601
26 let decoder = GitbayClient.decoder()
2827 return try decoder.decode(InboxNotification.self, from: Data(json.utf8))
2928}
3029
gitbayTests/ReleaseTests.swift +1 −2
@@ -161,8 +161,7 @@ struct ReleaseDetailViewModelTests {
161161struct FeedReleaseDestinationTests {
162162
163163 @Test func releaseEventsNavigateToTheRelease() throws {
164 let decoder = JSONDecoder()
165 decoder.dateDecodingStrategy = .iso8601
164 let decoder = GitbayClient.decoder()
166165 let event = try decoder.decode(FeedEvent.self, from: Data("""
167166 {"id":2,"repo":"krz/gitbay","actor":"cmc","kind":"release.created",\
168167 "data":{"tag":"v1.0.0"},"created_at":"2026-08-26T05:27:36Z"}
gitbayTests/SearchTests.swift +2 −4
@@ -29,11 +29,9 @@ private let mixedResults = """
2929
3030struct SearchResultDecodingTests {
3131
32 /// Matches the established direct-decode pattern in
33 /// `DiscoveryTests.swift`: a plain decoder with `.iso8601` dates.
32 /// Decodes with the client's own date strategy, as `DiscoveryTests` does.
3433 private func decode(_ json: String) throws -> [SearchResult] {
35 let decoder = JSONDecoder()
36 decoder.dateDecodingStrategy = .iso8601
34 let decoder = GitbayClient.decoder()
3735 return try decoder.decode([SearchResult].self, from: Data(json.utf8))
3836 }
3937
gitbayUITests/LiveSmokeUITests.swift +19 −5
@@ -39,6 +39,9 @@ final class LiveSmokeUITests: XCTestCase {
3939 default: break // follow the device, as a real launch does
4040 }
4141 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()
4245 if name.contains("testRepoManagementFlows") {
4346 deleteScratchRepo()
4447 }
@@ -242,7 +245,7 @@ final class LiveSmokeUITests: XCTestCase {
242245 createdRow.tap()
243246
244247 // A fresh MR has one revision; the section lists it.
245 let revisions = app.staticTexts["Revisions"].firstMatch
248 let revisions = header("Revisions")
246249 XCTAssertTrue(scrollTo(revisions, swipes: 8), "revisions section missing")
247250 XCTAssertTrue(app.staticTexts["v1"].firstMatch.exists, "first revision not listed")
248251
@@ -320,6 +323,12 @@ final class LiveSmokeUITests: XCTestCase {
320323 /// rows do not exist to XCUITest, so waitForExistence alone fails on
321324 /// anything below the fold.
322325 @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
323332 func scrollTo(_ element: XCUIElement, swipes: Int = 6) -> Bool {
324333 for _ in 0..<swipes {
325334 if element.exists { return true }
@@ -650,7 +659,7 @@ extension LiveSmokeUITests {
650659 XCTAssertTrue(app.staticTexts["warez for the public"].firstMatch
651660 .waitForExistence(timeout: 15), "profile did not load")
652661 XCTAssertTrue(app.staticTexts
653 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch
662 .containing(NSPredicate(format: "label BEGINSWITH[c] 'Repositories'")).firstMatch
654663 .waitForExistence(timeout: 10), "profile repos missing")
655664 back()
656665
@@ -862,6 +871,11 @@ extension LiveSmokeUITests {
862871 }
863872
864873 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 }
865879 let tokenField = app.secureTextFields.firstMatch
866880 guard tokenField.waitForExistence(timeout: 5) else { return }
867881 guard let token = ProcessInfo.processInfo.environment["GITBAY_UITEST_TOKEN"] else {
@@ -1010,7 +1024,7 @@ extension LiveSmokeUITests {
10101024 XCTAssertTrue(app.staticTexts
10111025 .containing(NSPredicate(format: "label CONTAINS 'Self-Hosting'")).firstMatch
10121026 .waitForExistence(timeout: 20), "profile description missing")
1013 XCTAssertTrue(app.staticTexts["Organizations"].firstMatch.exists,
1027 XCTAssertTrue(header("Organizations").exists,
10141028 "org memberships missing")
10151029 XCTAssertTrue(app.staticTexts["krz"].firstMatch.exists, "org row missing")
10161030
@@ -1027,7 +1041,7 @@ extension LiveSmokeUITests {
10271041 .containing(NSPredicate(format: "label CONTAINS 'contributions in the last year'")).firstMatch),
10281042 "activity graph missing")
10291043 XCTAssertTrue(scrollTo(app.staticTexts
1030 .containing(NSPredicate(format: "label BEGINSWITH 'Repositories'")).firstMatch),
1044 .containing(NSPredicate(format: "label BEGINSWITH[c] 'Repositories'")).firstMatch),
10311045 "repositories missing")
10321046
10331047 // --- a log entry opens its commit ---
@@ -1063,7 +1077,7 @@ extension LiveSmokeUITests {
10631077 XCTAssertTrue(main.waitForExistence(timeout: 20), "refs did not load")
10641078 XCTAssertTrue(app.staticTexts["default"].firstMatch.exists,
10651079 "the default branch is not marked")
1066 XCTAssertTrue(app.staticTexts["Tags"].firstMatch.exists, "tags section missing")
1080 XCTAssertTrue(header("Tags").exists, "tags section missing")
10671081 main.tap()
10681082 // Tapping a ref browses the repository there.
10691083 XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20),