docs/superpowers/plans/2026-09-22-1.3.0-profile-about.md

v1.14.1
gitbay-ios/docs/superpowers/plans/2026-09-22-1.3.0-profile-about.md rendered · source · history · blame · raw

786 lines · 33464 bytes

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 <owner>/.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.

    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.


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

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:

    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:

    /// Since gitbay v1.31.0 the about text is a file in <owner>/.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:

    @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:

    /// 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:

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 `<owner>/.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:

    /// `profile set` for the caller's own profile, `org profile <org>` 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:

    /// `profile set` for the caller's own profile, `org profile <org>` 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:

/// `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
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 <owner>/.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

        // The format picks the filename the about text renders under.
        #expect(profile.aboutFile == "about.org")

with

        // The file's own path picks the renderer, as it does on the web.
        #expect(profile.aboutPath == "profile/README.org")

In anOrgProfileCarriesMembersInsteadOfOrgs(), replace

        // Markdown is the default for anything but org.
        #expect(profile.aboutFile == "about.md")

with

        // 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

        /// 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

        /// Long-form profile text, rendered between the header and the
        /// activity graph the way the web renders it.
        let about: String?
        /// The file in `<name>/.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:

        /// 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

                ReadmeView(name: profile.aboutFile, content: about)

with

                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
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 ("<name>/.gitbay"), ProfileViewModel.createAboutFile() async -> Bool.

  • Step 1: Write the failing tests

Append to gitbayTests/ProfileCommitTests.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 <owner>/.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(_:):

    /// The repository that holds this profile's about text.
    var aboutRepo: String { "\(name)/.gitbay" }

    /// Creates `<name>/.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:

    /// 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
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 <owner>/.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:

        // --- 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
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:

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
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
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
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
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 <owner>/.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 <owner>/.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
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
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.

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 =<owner>/.gitbay=,"
new = "at =profile/README.md=\n  or =profile/README.org= in =<owner>/.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:

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.