# 1.3.0 Profile About Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Profile and org-profile saves work again against gitbay v1.31.0 and later, and the about text is edited as the file it now is. **Architecture:** `profile set` and `org profile` carry description, website and links only. The about text lives at `profile/README.{md,org,markdown}` in `/.gitbay`; `profile show` reports its path as `about_path`. The profile screen links to that file in the existing file screen, whose edit sheet already commits through `repo commit-file`, and offers to create the repository and a starter file when there is none — the web's `profile-repo` form. **Tech Stack:** Swift 6 language mode with default `MainActor` isolation, SwiftUI, Swift Testing. iOS 18.0 minimum, iOS 26.5 SDK. Server side: `krz/gitbay` wiki (org-mode) and the `gitbay` CLI. **Spec:** `docs/superpowers/specs/2026-09-22-parity-followup-design.md` (sections "1.3.0: profile about" and "Upstream: krz/gitbay") ## Global Constraints - **Never attribute anything to an assistant or model** — not in commits, code comments, MR bodies, or docs. No `Co-Authored-By` trailer. This is absolute. - **Commits are signed.** `commit.gpgsign` is on in both repositories; do not pass `--no-gpg-sign`. - **Never push to `main`. Never merge.** Work on the branches below; pushing a branch and opening its MR is the last step of a task group, and merging is the user's. - **Two repositories.** Tasks 1–4 land in `~/git/krz/gitbay-ios` on branch `profile-about`. Task 5 lands in `~/git/krz/gitbay` on branch `parity-ios-followup`. Check which one you are in before editing. - **Swift 6, default `MainActor` isolation.** Wire models are `nonisolated` structs; anything decoded needs `nonisolated` or its `Decodable` conformance fails as main-actor isolated. - **The Xcode project uses file-system-synchronized groups.** New `.swift` files under `gitbay/` and `gitbayTests/` are picked up automatically. Do not edit `project.pbxproj`. - **Reads are `GET /api/v1/read?argv=…`; writes are `POST /api/v1/cmd`.** In tests, a stub's `match:` is a URL substring: `"argv=profile&argv=show"`, `"argv=repo&argv=show"` for reads, `"cmd"` for writes. Writes are asserted by filtering `stub.seen` on `method == "POST"`, never by position. - **Exit 3 is `GitbayError.notFound`**, and `error.isEmptyState` is true for exactly that case. - Comments explain *why*, in plain direct English, at the density of the surrounding code. No before/after commentary. - **Run the whole unit target, and read the count.** `-only-testing:` on a single test can report success with zero tests run. Baseline on 2026-09-22: 462 tests, 461 passed, 1 skipped. ```bash cd /Users/cmc/git/krz/gitbay-ios && rm -rf /tmp/gb.xcresult && xcodebuild -project gitbay.xcodeproj -scheme gitbay \ -destination 'id=0A03DF15-0F45-4726-8080-DBFE919F46B6' -only-testing:gitbayTests \ -resultBundlePath /tmp/gb.xcresult test 2>&1 | grep -E "error:|failed|TEST (SUCCEEDED|FAILED)" | tail -20 xcrun xcresulttool get test-results summary --path /tmp/gb.xcresult --format json \ | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['result'], d['totalTestCount'], d['failedTests'])" ``` The destination is the iPhone 17 Pro simulator on iOS 26.5. Everything below that says "run the unit target" means these two commands. --- ### Task 1: `profile set` carries scalars and links only **Files:** - Modify: `gitbay/Discovery/ProfileEdit.swift` (whole file) - Modify: `gitbay/Discovery/ProfileViewModel.swift:136-143` (`saveProfile`) - Modify: `gitbay/Views/Discovery/ProfileEditSheet.swift:3-8` (doc comment), `:37-54` (About section) - Test: `gitbayTests/ProfileEditTests.swift` **Interfaces:** - Produces: `ProfileEdit(description: String = "", website: String = "", links: [ProfileEdit.Link] = [])`, `ProfileEdit(from: ProfileViewModel.Profile)`, `ProfileEdit.flags() -> [String]`. The properties `about`, `aboutFormat` and `aboutStdin` no longer exist. - [ ] **Step 1: Create the branch** ```bash cd /Users/cmc/git/krz/gitbay-ios && git switch main && git switch -c profile-about ``` - [ ] **Step 2: Rewrite the tests to the new contract** In `gitbayTests/ProfileEditTests.swift`: Replace the `edit(...)` helper at the top of `ProfileEditFlagTests` with: ```swift private func edit( description: String = "", website: String = "", links: [ProfileEdit.Link] = [] ) -> ProfileEdit { ProfileEdit(description: description, website: website, links: links) } ``` Delete `theAboutTextGoesToStdinNotArgv()` and `theAboutFormatAccompaniesTheText()`, and add in their place: ```swift /// Since gitbay v1.31.0 the about text is a file in /.gitbay, /// and `profile set` refuses any of these as an unknown flag (exit 2). @Test func noAboutFlagIsSent() { let flags = edit(description: "hi", website: "https://x.test", links: [.init(label: "a", url: "https://a.test")]).flags() #expect(!flags.contains("--about")) #expect(!flags.contains("--about-format")) #expect(!flags.contains("--file")) } ``` In `ProfileEditFromProfileTests`, replace `seedsFromAnOrgProfile()` with the version below and delete `seedingFallsBackToMarkdownWhenFormatIsAbsent()` entirely: ```swift @Test func seedsFromTheProfile() throws { let data = try #require(orgProfileJSON.data(using: .utf8)) let profile = try JSONDecoder().decode(ProfileViewModel.Profile.self, from: data) let edit = ProfileEdit(from: profile) #expect(edit.description == "about me") #expect(edit.website == "https://cleberg.net") #expect(edit.links.map(\.url) == ["https://a.test", "https://b.test"]) #expect(edit.links.map(\.label) == ["", "blog"]) } ``` In `ProfileSaveTests`, replace `savesTheFullArgvWithAboutOnlyInStdin()` with: ```swift /// The write, then the reload `profile set` triggers — one POST, one /// GET. Both need a stub or the flow ends up looking like a refusal /// even when the write itself succeeded. @Test func savesScalarsAndLinksWithNoStdin() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: profileJSON, match: "argv=show")) let model = ProfileViewModel(client: client, name: "cmc") let edit = ProfileEdit( description: "hi", website: "https://cleberg.net", links: [.init(label: "blog", url: "https://b.test")] ) let saved = await model.saveProfile(edit) #expect(saved) let write = try #require(stub.seen.first { $0.method == "POST" }) let (argv, stdin) = try argvOf(write) #expect(argv == [ "profile", "set", "--description", "hi", "--website", "https://cleberg.net", "--link", "blog|https://b.test", ]) #expect(stdin == nil) } ``` - [ ] **Step 3: Run the unit target to verify it fails** Run the unit target (see Global Constraints). Expected: `Failed 460 2`. The tests compile — every parameter of the current initializer has a default — and two fail at runtime: `noAboutFlagIsSent` (the flags still end in `--about-format md --file -`) and `savesScalarsAndLinksWithNoStdin` (the argv carries that tail and stdin is `""`). - [ ] **Step 4: Rewrite `ProfileEdit`** Replace the whole of `gitbay/Discovery/ProfileEdit.swift` with: ```swift import Foundation /// `profile set` — unlike every other filter or edit in the app, `--link` /// replaces the whole set rather than appending, so every link the form /// wants to keep is sent on every save. The form is always seeded from /// the current profile, so every other field is also sent every time, /// including empty ones: `--description ""` clears, omitting the flag /// would leave the old value alone. /// /// The about text is not here. It is a file in `/.gitbay`, written /// through the file editor like any other file. nonisolated struct ProfileEdit: Equatable, Sendable { nonisolated struct Link: Equatable, Sendable, Identifiable { var id = UUID() var label: String var url: String } var description: String var website: String var links: [Link] init(description: String = "", website: String = "", links: [Link] = []) { self.description = description self.website = website self.links = links } init(from profile: ProfileViewModel.Profile) { description = profile.description ?? "" website = profile.website ?? "" links = (profile.links ?? []).map { Link(label: $0.label ?? "", url: $0.url) } } /// At most 5 links and every link needs a URL — the server enforces /// both, but the form should not let a save reach it that way. A /// label may not contain `|` either: the server splits a link on its /// first pipe, so a label carrying one would silently corrupt the URL /// rather than be rejected. var validationError: String? { if links.count > 5 { return "At most 5 links." } if links.contains(where: { $0.url.trimmingCharacters(in: .whitespaces).isEmpty }) { return "A link needs a URL." } if links.contains(where: { $0.label.contains("|") }) { return "A link label can't contain \"|\" — that's what separates it from the URL." } return nil } func flags() -> [String] { var flags = ["--description", description, "--website", website] if links.isEmpty { flags += ["--link", ""] } else { for link in links { let label = link.label.trimmingCharacters(in: .whitespaces) flags += ["--link", label.isEmpty ? link.url : "\(label)|\(link.url)"] } } return flags } } ``` - [ ] **Step 5: Stop sending stdin from `saveProfile`** In `gitbay/Discovery/ProfileViewModel.swift`, replace: ```swift /// `profile set` for the caller's own profile, `org profile ` for /// an organization; both take the same flags. The about text travels /// over stdin, never argv, since it is long-form and may carry newlines. func saveProfile(_ edit: ProfileEdit) async -> Bool { guard edit.validationError == nil else { return false } let command = state.value?.isOrg == true ? ["org", "profile", name] : ["profile", "set"] await perform(command + edit.flags(), stdin: edit.aboutStdin) return actionError == nil } ``` with: ```swift /// `profile set` for the caller's own profile, `org profile ` for /// an organization; both take the same flags. func saveProfile(_ edit: ProfileEdit) async -> Bool { guard edit.validationError == nil else { return false } let command = state.value?.isOrg == true ? ["org", "profile", name] : ["profile", "set"] await perform(command + edit.flags()) return actionError == nil } ``` - [ ] **Step 6: Remove the about field from the sheet** In `gitbay/Views/Discovery/ProfileEditSheet.swift`, replace the doc comment on the struct with: ```swift /// `profile set`, or `org profile` for an organization. Description and /// website are single lines; links are capped at five, matching the /// server. The about text is a file and is edited from the profile /// screen, not here. ``` and delete the whole `Section { Picker("Format", …) … } header: { Text("About") } footer: { … }` block (the section between the Website section and `linksSection`). - [ ] **Step 7: Run the unit target to verify it passes** Run the unit target. Expected: `Passed 460 0` — two about tests and one seeding test deleted, one flag test added. - [ ] **Step 8: Commit** ```bash cd /Users/cmc/git/krz/gitbay-ios && git add gitbay/Discovery/ProfileEdit.swift gitbay/Discovery/ProfileViewModel.swift gitbay/Views/Discovery/ProfileEditSheet.swift gitbayTests/ProfileEditTests.swift && git commit -m "profile: set description, website and links only gitbay v1.31.0 moved the about text into /.gitbay, and profile set and org profile refuse --about-format and --file as unknown flags, so every save failed with exit 2." ``` --- ### Task 2: Render the about text under the file it came from **Files:** - Modify: `gitbay/Discovery/ProfileViewModel.swift:11-48` (the `Profile` struct) - Modify: `gitbay/Views/Discovery/ProfileView.swift:140-149` (`aboutSection`) - Test: `gitbayTests/ProfileCommitTests.swift` **Interfaces:** - Consumes: nothing from Task 1. - Produces: `ProfileViewModel.Profile.aboutPath: String?` (decoded from `about_path`). `aboutFormat` and `aboutFile` no longer exist. - [ ] **Step 1: Write the failing test** In `gitbayTests/ProfileCommitTests.swift`, in `userProfileJSON`, change ``` "about":"* Christian\\n\\nOrg-Mode.","about_format":"org",\ ``` to ``` "about":"* Christian\\n\\nOrg-Mode.","about_format":"org","about_path":"profile/README.org",\ ``` In `aUserProfileCarriesOrgsReposAndActivity()`, replace ```swift // The format picks the filename the about text renders under. #expect(profile.aboutFile == "about.org") ``` with ```swift // The file's own path picks the renderer, as it does on the web. #expect(profile.aboutPath == "profile/README.org") ``` In `anOrgProfileCarriesMembersInsteadOfOrgs()`, replace ```swift // Markdown is the default for anything but org. #expect(profile.aboutFile == "about.md") ``` with ```swift // No about file, no path. #expect(profile.aboutPath == nil) ``` - [ ] **Step 2: Run the unit target to verify it fails** Expected: build FAILS with `value of type 'ProfileViewModel.Profile' has no member 'aboutPath'`. - [ ] **Step 3: Decode `about_path`, drop the format guess** In `gitbay/Discovery/ProfileViewModel.swift`, in `Profile`: Replace ```swift /// Long-form profile text, rendered between the header and the /// activity graph the way the web renders it. let about: String? let aboutFormat: String? ``` with ```swift /// Long-form profile text, rendered between the header and the /// activity graph the way the web renders it. let about: String? /// The file in `/.gitbay` the text was read from, e.g. /// `profile/README.org`. Its extension picks the renderer. let aboutPath: String? ``` In `CodingKeys`, replace `case aboutFormat = "about_format"` with `case aboutPath = "about_path"`. Delete the `aboutFile` property and its doc comment: ```swift /// The filename the about text is rendered under. The server has /// none to dispatch on either, so the stored format picks the /// extension and anything but org is markdown. var aboutFile: String { aboutFormat == "org" ? "about.org" : "about.md" } ``` - [ ] **Step 4: Render under the real path** In `gitbay/Views/Discovery/ProfileView.swift`, in `aboutSection`, replace ```swift ReadmeView(name: profile.aboutFile, content: about) ``` with ```swift ReadmeView(name: profile.aboutPath ?? "", content: about) ``` `ReadmeView` renders anything not ending in `.org` as markdown, so an absent path renders markdown. - [ ] **Step 5: Run the unit target to verify it passes** Expected: `Passed 460 0`. - [ ] **Step 6: Commit** ```bash cd /Users/cmc/git/krz/gitbay-ios && git add gitbay/Discovery/ProfileViewModel.swift gitbay/Views/Discovery/ProfileView.swift gitbayTests/ProfileCommitTests.swift && git commit -m "profile: render the about text under about_path" ``` --- ### Task 3: Edit the about file, or create it **Files:** - Modify: `gitbay/Discovery/ProfileViewModel.swift` (add `aboutRepo`, `createAboutFile()`) - Modify: `gitbay/Views/Discovery/ProfileView.swift` (`aboutSection`) - Test: `gitbayTests/ProfileCommitTests.swift` (new suite `AboutFileTests` at the end of the file) **Interfaces:** - Consumes: `Profile.aboutPath` (Task 2), `Profile.canEdit(by:)`, `RepoRoute.file(repo: String, path: String, ref: String?)`. - Produces: `ProfileViewModel.aboutRepo: String` (`"/.gitbay"`), `ProfileViewModel.createAboutFile() async -> Bool`. - [ ] **Step 1: Write the failing tests** Append to `gitbayTests/ProfileCommitTests.swift`: ```swift private func postArgv(_ stub: StubProtocol.Box) throws -> [([String], String?)] { try stub.seen.filter { $0.method == "POST" }.map { seen in let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return (try #require(body["argv"] as? [String]), body["stdin"] as? String) } } private let okCmdJSON = #"{"protocol_version":1,"exit_code":0}"# @MainActor struct AboutFileTests { /// The web's "profile-repo" form: create /.gitbay, commit a /// starter README, show the result. @Test func createsTheRepositoryAndAStarterFile() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no repository cmc/.gitbay","exit_code":3}"#, match: "argv=repo&argv=show")) stub.enqueue(.init(status: 200, json: okCmdJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: okCmdJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: userProfileJSON, match: "argv=profile&argv=show")) let model = ProfileViewModel(client: client, name: "cmc") let created = await model.createAboutFile() #expect(created) #expect(model.actionError == nil) let writes = try postArgv(stub) #expect(writes.map(\.0) == [ ["repo", "create", "cmc/.gitbay"], ["repo", "commit-file", "cmc/.gitbay", "profile/README.md", "--ref", "main", "--message", "add profile about", "--file", "-"], ]) #expect(writes[1].1 == "# cmc\n\nThis is the about text on your profile.\n") #expect(model.state.value?.aboutPath == "profile/README.org") } /// A .gitbay repository can exist without an about file in it — it /// holds other per-owner configuration too. Creating it again would /// fail, so only the file is written. @Test func skipsCreateWhenTheRepositoryExists() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"path":"krz/.gitbay"},"exit_code":0}"#, match: "argv=repo&argv=show")) stub.enqueue(.init(status: 200, json: okCmdJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: orgProfileJSON, match: "argv=profile&argv=show")) let model = ProfileViewModel(client: client, name: "krz") let created = await model.createAboutFile() #expect(created) let writes = try postArgv(stub) #expect(writes.count == 1) #expect(Array(writes[0].0.prefix(4)) == ["repo", "commit-file", "krz/.gitbay", "profile/README.md"]) #expect(writes[0].1 == "# krz\n\nThis is the about text on your profile.\n") } /// A refused create must not go on to write a file into nothing. @Test func aRefusedCreateStopsAndSurfaces() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no repository krz/.gitbay","exit_code":3}"#, match: "argv=repo&argv=show")) stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"error":"only admins of krz can create repositories there","exit_code":4}"#, match: "cmd")) let model = ProfileViewModel(client: client, name: "krz") let created = await model.createAboutFile() #expect(created == false) #expect(model.actionError?.isEmpty == false) #expect(try postArgv(stub).count == 1) } @Test func theAboutRepositoryIsTheOwnersDotGitbay() throws { let (client, _) = try makeClient() #expect(ProfileViewModel(client: client, name: "cmc").aboutRepo == "cmc/.gitbay") } } ``` - [ ] **Step 2: Run the unit target to verify it fails** Expected: build FAILS with `value of type 'ProfileViewModel' has no member 'createAboutFile'` and `… no member 'aboutRepo'`. - [ ] **Step 3: Implement `createAboutFile`** In `gitbay/Discovery/ProfileViewModel.swift`, add after `saveProfile(_:)`: ```swift /// The repository that holds this profile's about text. var aboutRepo: String { "\(name)/.gitbay" } /// Creates `/.gitbay` when it is missing and commits a starter /// `profile/README.md`, the same starter the web's settings page /// writes, so the file editor has something to open. The repository /// may already exist without the file, and creating it again would /// fail, so it is looked up first. func createAboutFile() async -> Bool { working = true actionError = nil defer { working = false } do { do { _ = try await client.read(["repo", "show", aboutRepo], as: Exists.self) } catch let error as GitbayError where error.isEmptyState { try await client.run(["repo", "create", aboutRepo]) } try await client.run( ["repo", "commit-file", aboutRepo, "profile/README.md", "--ref", "main", "--message", "add profile about", "--file", "-"], stdin: "# \(name)\n\nThis is the about text on your profile.\n") await load() return true } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } return false } /// `repo show` is read only to learn whether the repository exists. nonisolated private struct Exists: Decodable, Sendable {} ``` - [ ] **Step 4: Run the unit target to verify it passes** Expected: `Passed 464 0`. - [ ] **Step 5: Add the Edit and Create actions to the about section** In `gitbay/Views/Discovery/ProfileView.swift`, replace the whole `aboutSection` function with: ```swift /// The long-form about text, rendered under the file it came from. A /// viewer who may write the profile gets the file itself to edit, or, /// when there is none yet, the offer to create it. @ViewBuilder private func aboutSection(_ profile: ProfileViewModel.Profile) -> some View { let canEdit = profile.canEdit(by: session.current?.username) if let about = profile.about, !about.isEmpty { Section("About") { ReadmeView(name: profile.aboutPath ?? "", content: about) .padding(.vertical, 4) if canEdit, let path = profile.aboutPath { NavigationLink(value: RepoRoute.file(repo: model.aboutRepo, path: path, ref: nil)) { Label("Edit About", systemImage: "pencil") } .accessibilityIdentifier("profile-about-edit") } } } else if canEdit { Section { Button { Task { await model.createAboutFile() } } label: { Label("Create About File", systemImage: "doc.badge.plus") } .disabled(model.working) .accessibilityIdentifier("profile-about-create") } header: { Text("About") } footer: { Text("Adds profile/README.md to \(model.aboutRepo), creating the repository if needed.") } } } ``` - [ ] **Step 6: Build and run the unit target** Expected: `Passed 464 0`. The view has no unit test; Task 4's live step covers it. - [ ] **Step 7: Commit** ```bash cd /Users/cmc/git/krz/gitbay-ios && git add gitbay/Discovery/ProfileViewModel.swift gitbay/Views/Discovery/ProfileView.swift gitbayTests/ProfileCommitTests.swift && git commit -m "profile: edit the about file, or create it The about section links to profile/README.* in /.gitbay for a viewer who may write it, and offers to create the repository and a starter file when there is none, as the web's settings page does." ``` --- ### Task 4: Live suite covers the save and the about actions **Files:** - Modify: `gitbayUITests/LiveSmokeUITests.swift` (`testProfileAndNavigationFlows`, after the "a profile is a profile" block, before "an org admin can edit the org's profile") **Interfaces:** - Consumes: accessibility identifiers `profile-edit-button`, `profile-edit-save` (existing), `profile-about-edit`, `profile-about-create` (Task 3). `Self.smokeProfileDescription`. The `ios-smoke` account's `.gitbay` repository may or may not exist, so the step accepts either action and creates nothing. - [ ] **Step 1: Add the steps** In `testProfileAndNavigationFlows()`, immediately before the line `// --- an org admin can edit the org's profile ---`, insert: ```swift // --- saving the profile succeeds: profile set takes no about flags --- let edit = app.descendants(matching: .any) .matching(identifier: "profile-edit-button").firstMatch XCTAssertTrue(edit.waitForExistence(timeout: 10), "own profile offers no edit") edit.tap() let save = app.buttons["profile-edit-save"].firstMatch XCTAssertTrue(save.waitForExistence(timeout: 10), "edit sheet did not open") XCTAssertFalse(app.descendants(matching: .any) .matching(identifier: "profile-edit-about").firstMatch.exists, "the sheet still edits the about text") save.tap() XCTAssertTrue(save.waitForNonExistence(timeout: 15), "profile save did not dismiss the sheet — the server refused it") // --- the about text is a file: edit it, or offer to create it --- let aboutAction = app.descendants(matching: .any).matching(NSPredicate( format: "identifier IN %@", ["profile-about-edit", "profile-about-create"])).firstMatch XCTAssertTrue(scrollTo(aboutAction), "own profile offers neither editing nor creating the about file") ``` - [ ] **Step 2: Build the UI test target** ```bash cd /Users/cmc/git/krz/gitbay-ios && xcodebuild -project gitbay.xcodeproj -scheme gitbay \ -destination 'id=0A03DF15-0F45-4726-8080-DBFE919F46B6' build-for-testing 2>&1 | grep -E "error:|BUILD (SUCCEEDED|FAILED)" | tail -5 ``` Expected: `** TEST BUILD SUCCEEDED **`. `waitForNonExistence(timeout:)` needs the iOS 26.5 SDK's XCTest, which is what builds here. - [ ] **Step 3: Run the live test (needs a token from the user)** The live suite runs as `ios-smoke` and needs `TEST_RUNNER_GITBAY_UITEST_TOKEN`. Ask the user to mint one — `ssh -F /dev/null -i ~/.ssh/gitbay-ios-smoke -o IdentitiesOnly=yes git@gitbay.org token create --name ui-smoke --scope full --ttl 1d` — and never type it into the app by hand. Then: ```bash cd /Users/cmc/git/krz/gitbay-ios && TEST_RUNNER_GITBAY_UITEST_LIVE=1 TEST_RUNNER_GITBAY_UITEST_TOKEN="$TOKEN" \ xcodebuild -project gitbay.xcodeproj -scheme gitbay \ -destination 'id=0A03DF15-0F45-4726-8080-DBFE919F46B6' \ -only-testing:gitbayUITests/LiveSmokeUITests/testProfileAndNavigationFlows \ -resultBundlePath /tmp/gb-live.xcresult test 2>&1 | grep -E "error:|passed|failed|TEST (SUCCEEDED|FAILED)" | tail -10 ``` Expected: `Test Case '-[gitbayUITests.LiveSmokeUITests testProfileAndNavigationFlows]' passed`. If it fails, read the failure from the bundle (`xcrun xcresulttool get test-results tests --path /tmp/gb-live.xcresult --format json`), not stdout. If no token is available, record that the live step was not run and continue. - [ ] **Step 4: Commit** ```bash cd /Users/cmc/git/krz/gitbay-ios && git add gitbayUITests/LiveSmokeUITests.swift && git commit -m "live suite: profile save and the about file actions" ``` - [ ] **Step 5: Run the unit target on iOS 18.6 as well** ```bash cd /Users/cmc/git/krz/gitbay-ios && rm -rf /tmp/gb18.xcresult && xcodebuild -project gitbay.xcodeproj -scheme gitbay \ -destination 'id=9F8E5D84-0A15-4C64-8A45-994667A73817' -only-testing:gitbayTests \ -resultBundlePath /tmp/gb18.xcresult test 2>&1 | grep -E "error:|TEST (SUCCEEDED|FAILED)" | tail -5 ``` Expected: `** TEST SUCCEEDED **`, same count as on iOS 26.5. - [ ] **Step 6: Release build for device** ```bash cd /Users/cmc/git/krz/gitbay-ios && xcodebuild build -scheme gitbay -configuration Release \ -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO 2>&1 | grep -E "error:|While running pass|BUILD (SUCCEEDED|FAILED)" | tail -5 ``` Expected: `** BUILD SUCCEEDED **`. - [ ] **Step 7: Push the branch and open the MR** ```bash cd /Users/cmc/git/krz/gitbay-ios && git push -u origin profile-about && gitbay mr create --source profile-about --target main \ --title "Profile save: drop the about flags, edit the about file" --file - <<'EOF' gitbay v1.31.0 moved the profile about text into /.gitbay and removed --about-format and --file from profile set and org profile. Every save from the app failed with exit 2 (unknown flag). - ProfileEdit sends description, website and links only. - Profile decodes about_path and renders the text under it. - The about section links to the file for a viewer who may write it, and offers to create /.gitbay with a starter profile/README.md when there is none. Spec: docs/superpowers/specs/2026-09-22-parity-followup-design.md EOF ``` Main already carries 1.3.0 (12), so no version bump is needed. Merging, tagging `v1.3.0` on the merge commit, archiving and uploading are the user's. --- ### Task 5: Upstream corrections in krz/gitbay **Files:** - Modify: `~/git/krz/gitbay/.gitbay/wiki/Parity.org:204` (the `build list paging` row), the rows after it, and the preview paragraph ending near line 159. - The published v1.31.0 release notes (not a file; edited with `gitbay release edit`). **Interfaces:** none. - [ ] **Step 1: Branch** ```bash cd /Users/cmc/git/krz/gitbay && git switch main && git pull --ff-only && git switch -c parity-ios-followup ``` - [ ] **Step 2: Correct the build rows** In `.gitbay/wiki/Parity.org`, replace ``` | build list paging (limit, cursor) | yes | yes | yes | ``` with ``` | build list paging (limit, cursor) | yes | yes | no | | build row names its commit | yes | yes | no | ``` - [ ] **Step 3: Say what the iOS preview will be** In the same file, the preview paragraph ends with ``` =issue show= already renders. Diff-line comments are left out — they sit inside the diff, where a page-level preview has nowhere to go. ``` Append after it, as a new paragraph: ``` The iOS client resolves no autolinks: =#N= and =owner/name#N= render as text in its threads, so a preview there will show the app's rendering rather than the one the web page shows. ``` - [ ] **Step 4: Commit and open the MR** ```bash cd /Users/cmc/git/krz/gitbay && git add .gitbay/wiki/Parity.org && git commit -m "Parity: iOS build list paging is no; add the commit-subject row The iOS build list reads one unpaged page and leads each row with a sha. Also note that the iOS client resolves no autolinks." && git push -u origin parity-ios-followup && \ gitbay mr create --source parity-ios-followup --target main --title "Parity: correct the iOS build rows" --file - <<'EOF' - build list paging (limit, cursor): iOS is no. The client reads one page and ignores the cursor. - New row: build row names its commit (the subject field from v1.33.0). iOS is no. - A sentence that the iOS client resolves no autolinks, ahead of its markup preview. EOF ``` - [ ] **Step 5: Correct the v1.31.0 release notes (confirm with the user first)** This edits a published release on gitbay.org. Show the user the before/after sentence and wait for a yes before running it. ```bash cd /Users/cmc/git/krz/gitbay && gitbay release show krz/gitbay v1.31.0 --json \ | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(d['notes'])" > /tmp/v1.31.0-notes.org python3 - <<'EOF' p = '/tmp/v1.31.0-notes.org' s = open(p).read() old = "at =about.org= or\n =about.md= in =/.gitbay=," new = "at =profile/README.md=\n or =profile/README.org= in =/.gitbay=," assert old in s, "sentence not found; the notes changed since the audit" open(p, 'w').write(s.replace(old, new)) EOF diff <(gitbay release show krz/gitbay v1.31.0 --json | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['notes'])") /tmp/v1.31.0-notes.org ``` Expected: the diff shows only the two changed lines. After the user confirms: ```bash cd /Users/cmc/git/krz/gitbay && gitbay release edit krz/gitbay v1.31.0 --file - < /tmp/v1.31.0-notes.org && \ gitbay release show krz/gitbay v1.31.0 | grep -n "profile/README" ``` No `--format`: `release edit` keeps the stored format when none is given. Expected: the line naming `profile/README.md` prints. The CHANGELOG entry for v1.31.0 already names the right path and is not touched.