docs/superpowers/specs/2026-09-22-parity-followup-design.md

v1.11.0
gitbay-ios/docs/superpowers/specs/2026-09-22-parity-followup-design.md rendered · source · history · blame · raw

220 lines · 10858 bytes

Parity follow-up: gitbay v1.28.1 to v1.34.1

An audit on 2026-09-22 of the Parity wiki page (krz/gitbay, .gitbay/wiki/Parity.org) and of every gitbay release since the app's v1.2.0 (server v1.28.1 through v1.34.1, the last unreleased on main) found one regression, three gaps the page records, two it does not, and one row that is wrong. This spec covers all of them in four releases.

Push notifications (v1.32.0, v1.32.1) are already done on main and are not restated here; see 2026-09-20-push-notifications-design.md.

Findings

# Finding Source Kind
1 Profile and org-profile save fail: unknown flag "--about-format", exit 2 v1.31.0 regression
2 build list returns subject; the app does not decode it v1.33.0 unrowed gap
3 build list pages with --limit/--cursor; the app does not v1.33.0 wrong row (page says yes)
4 MR labels: mr label --add/--remove page gap
5 MR filter by label: mr list --label page gap
6 Preview body markup: issue, MR, file editor, release notes page, v1.30.0 gap (4 rows)
7 Admin: account list and state filter, promote/demote, disable/enable, worker queues page, v1.30.0 gap (4 rows)

Finding 1 was confirmed against gitbay.org. ProfileEdit.flags() appends --about-format <f> --file - on every save, and parseProfileFlags in internal/control/profile.go accepts only --description, --website and --link. The shipped 1.2.0 is affected.

Releases

Version Carries
1.3.0 (12) push (on main) and finding 1
1.4.0 (13) findings 2–5
1.5.0 (14) finding 6
1.6.0 (15) finding 7

Each is a squash-merged MR per feature, then a bump MR touching the four MARKETING_VERSION/CURRENT_PROJECT_VERSION lines, then a lightweight tag on the bump's merge commit.

1.3.0: profile about

The about text has been a file since v1.31.0: profile/README.md, .org or .markdown, resolved in that order, on the default branch of <owner>/.gitbay. profile show returns it as about, about_format and about_path. profile set and org profile take description, website and links only.

The web's account page no longer edits the text. It links to the file editor and, when there is no file, offers to create the repository with a starter README (internal/httpd/account.go, the profile-repo form). The app does the same.

  • ProfileEdit drops about, aboutFormat and aboutStdin, and flags() drops the --about-format/--file - tail. ProfileViewModel.saveProfile stops passing stdin. The same argv serves profile set and org profile <org>.
  • ProfileEditSheet drops the about TextEditor and format picker.
  • Profile decodes about_path. aboutFile is deleted; ReadmeView receives aboutPath, so the file's extension picks the renderer.
  • ProfileView's about section, for a viewer where canEdit is true, gains an Edit action that pushes .file(repo: "<owner>/.gitbay", path: aboutPath, ref: nil). The file screen's existing edit action and repo commit-file do the write.
  • With no about_path and canEdit true, the section offers "Create about file". It runs repo show <owner>/.gitbay; on exit 3 (not found) it runs repo create <owner>/.gitbay. It then runs repo commit-file <owner>/.gitbay profile/README.md --ref <branch> --message "add profile about" --file -, where <branch> is the existing repository's default_branch from repo show (the server reads the about file from the default branch) or main for a repository just created, with the web's starter text (# <owner>\n\nThis is the about text on your profile.\n), and reloads. Any other failure is shown and stops the flow. repo create accepts <org>/.gitbay from an org admin (resolveNewRepoOwner, internal/control/repo.go), so the offer is the same for users and organizations.

Tests: ProfileEdit.flags() carries no --about-format or --file; saveProfile sends no stdin; Profile decodes about_path; the create flow issues repo create then repo commit-file with the starter on stdin, and skips repo create when repo show succeeds. The live suite's profile step saves a description and asserts success.

1.4.0: build list and MR labels

Build list

  • Build gains subject: String?.
  • BuildRow leads with #<n> <subject>, falling back to #<n> <shortSHA> when the commit is gone. The meta line carries job, ref and shortSHA.
  • BuildListViewModel replaces readList with PagedListModel<Build>(pageSize: 30), the web's page size. The sorted { $0.number > $1.number } goes: cursor pages arrive in server order and a client sort would reorder across page boundaries. A filter change sets argv and calls reload().
  • BuildListView gains PageFooter, as the issue and MR lists have.

load() never set .loading before; reload tests wait on the final state, not on "not loading".

MR labels

A copy of the issue implementation.

  • MRDetailViewModel loads label list <repo> for the chips' colours and gains addLabel/removeLabel, sending mr label <repo> <n> --add|--remove <l>, as IssueDetailViewModel does for issue label.
  • MRView shows the label chips, each removable, and the add field IssueView has.
  • MRFilter gains label: String?, rendered as --label. Its doc comment, which says the command takes no --label, is corrected. The MR filter sheet gains the label picker the issue sheet has.

Tests: Build decodes with and without subject; the row title falls back; the build list sends --limit 30, appends the second page on loadMore, and does not reorder; MRFilter.flags() with a label; mr label argv for add and remove.

1.5.0: markup previews

The web previews by posting the draft back to its own form, where the server renders it with autolinks resolved for the viewer. No command renders markup (the CLI row is n/a), and the app resolves no autolinks anywhere. The app previews locally with FormattedBodyView, the view its threads, bodies and comments already render with. The preview therefore matches what the app shows, not what the web shows: #N stays plain text in both the preview and the thread.

  • New MarkupPreview(text:format:identifier:) in gitbay/Views/Shared/: a collapsed "Preview" disclosure row that renders FormattedBodyView(text:format:) when expanded, and "Nothing to preview." for blank text. It sits under the editor rather than replacing it, so the draft stays visible and editable, as on the web.
  • MarkupPreview.format(forFile:) maps a file name to the format its contents render in: org for .org, md for .md and .markdown, nil for anything else.
  • Call sites, each passing the format it already sends:
    • ComposeSheet's body: issue create, issue and MR edit, release edit. The format follows the sheet's format binding, which is md where the sheet shows no picker.
    • MRCreateSheet in MRListView: its own format.
    • The comment rows in IssueView and MRView: their commentFormat picker. Shown only while the comment is non-empty.
    • ReleaseCreateSheet in ReleaseListView: md, since release create sends no --format and the app renders notes as markdown.
    • FileEditSheet: only when format(forFile:) is non-nil, with that format.

Tests: format(forFile:) for .org, .md, .markdown, .MD, .swift and a name without an extension. The live suite types a comment on the fixture issue, expands its preview and finds the rendered text, then clears the field without sending.

1.6.0: admin

Matches the web's /admin and /admin/users and nothing else. Account show, invite, runners, repository administration, the audit log and instance statistics stay no on both surfaces.

  • Who sees it. whoami returns admin. The My Profile tab reads it when it appears (and again on an account switch) and passes it to AccountMenu, which shows Admin only when it is true. The menu has no model of its own and the dashboard's data lives on another tab, so this is the one read that reaches it; SessionStore is unchanged.
  • The admin screen reads dashboard and decodes its admin-only queues and server blocks, the read the web's /admin page makes (internal/httpd/admin.go). It shows the server commit, a link to Accounts, and the web's six sections in the web's order — webhook deliveries, mail, push, mirrors, builds, dependency checks — each with its heading count, its counts line, its retrying or failed rows, and the web's empty-queue sentence. A dashboard without queues (the caller is not an admin) is an empty state saying so.
  • Accounts pages admin user list [--state active|pending|disabled|admin] through PagedListModel, with the web's five filters (all, active, pending, disabled, admins) as a segmented picker. Each row carries the web's two actions, in a per-row menu: Demote for an admin, Promote for an active account and a disabled Promote for anyone else; Enable for a disabled account and Disable otherwise. Each sends admin user promote|demote|disable|enable <username> and reloads.
  • Demote and disable ask for the username to be typed before they send, as the web does. Promote and enable ask with a plain alert.

Tests: the six sections' headings, counts, summaries and row text from a recorded payload; a dashboard without queues; whoami's admin, including a failed read counting as not admin; each row state's two actions; admin user list argv per filter and across a page; each write's argv and its reload; a refusal surfacing. The live suite asserts that ios-smoke, which is not an admin, is not offered Admin.

Upstream: krz/gitbay

One MR alongside 1.3.0:

  • Parity: iOS build list paging (limit, cursor) from yes to no; a row for "build row names its commit" (cli yes, web yes, ios no); a sentence under the preview paragraph that the iOS preview is the app's own rendering and resolves no autolinks.
  • The published v1.31.0 release notes give the about file as about.org or about.md; they are edited with release edit to profile/README.md or profile/README.org. The CHANGELOG entry for v1.31.0 already names the right path and is unchanged.

Later MRs flip rows as each release ships: the build rows at 1.4.0, MR labels and filter by label at 1.4.0, the four preview rows at 1.5.0, and account list, promote/demote, disable/enable and worker queues at 1.6.0.

Not in scope

  • Autolink resolution in the app.
  • A server-side render command.
  • Admin beyond what the web carries.
  • Bookmarks as a profile section: the app keeps them as their own screen, and the Parity row stays no.