Commit 42a9918acf

42a9918acf2b0949061115c3d667ebf375b51c78

parent: 3958f6060f

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-16 03:24 UTC

Secrets and deploy keys: reload after a refusal, a multi-line value, the sheet's own errors

A refused write reloads the list, so a row removed elsewhere goes. A secret can be entered on several lines, for a private key. A sheet shows only the error from its own attempt.

Ref #42
Ref #43

Layout: unified · split

gitbay/Repos/RepoCredentials.swift +6
@@ -54,6 +54,9 @@ final class BuildSecretsViewModel {
5454 } catch {
5555 actionError = GitbayError.transport(error).userFacingMessage
5656 }
57 // A refusal may mean the list changed elsewhere, e.g. a row
58 // removed from another device.
59 await load()
5760 return false
5861 }
5962}
@@ -134,6 +137,9 @@ final class DeployKeysViewModel {
134137 } catch {
135138 actionError = GitbayError.transport(error).userFacingMessage
136139 }
140 // A refusal may mean the list changed elsewhere, e.g. a row
141 // removed from another device.
142 await load()
137143 return false
138144 }
139145}
gitbay/Views/Repos/RepoCredentialViews.swift +28 −8
@@ -68,6 +68,9 @@ private struct SecretSetSheet: View {
6868
6969 @State private var name = ""
7070 @State private var value = ""
71 @State private var multiLine = false
72 /// Errors shown here are this sheet's own, not an earlier list action's.
73 @State private var attempted = false
7174
7275 var body: some View {
7376 NavigationStack {
@@ -78,14 +81,26 @@ private struct SecretSetSheet: View {
7881 .autocorrectionDisabled()
7982 .textInputAutocapitalization(.characters)
8083 .accessibilityIdentifier("secret-name")
81 SecureField("Value", text: $value)
82 .autocorrectionDisabled()
83 .textInputAutocapitalization(.never)
84 .accessibilityIdentifier("secret-value")
84 if multiLine {
85 TextEditor(text: $value)
86 .frame(minHeight: 120)
87 .font(.gbMono(.caption))
88 .autocorrectionDisabled()
89 .textInputAutocapitalization(.never)
90 .accessibilityIdentifier("secret-value-multiline")
91 } else {
92 SecureField("Value", text: $value)
93 .autocorrectionDisabled()
94 .textInputAutocapitalization(.never)
95 .accessibilityIdentifier("secret-value")
96 }
97 Toggle("Multi-line value", isOn: $multiLine)
8598 } footer: {
86 Text("Uppercase letters, digits and underscores. The value is sent once and cannot be read back.")
99 Text(multiLine
100 ? "Shown while you type, for a value with line breaks such as a private key. It is sent once and cannot be read back."
101 : "Uppercase letters, digits and underscores. The value is sent once and cannot be read back.")
87102 }
88 if let error = model.actionError {
103 if attempted, let error = model.actionError {
89104 Section { GBNotice(error, .gbWarn) }
90105 }
91106 }
@@ -101,6 +116,7 @@ private struct SecretSetSheet: View {
101116 } else {
102117 Button("Set") {
103118 Task {
119 attempted = true
104120 guard await model.set(name: name, value: value) else { return }
105121 // A secure field that still holds text as the sheet
106122 // goes makes iOS offer to save it as a password.
@@ -222,6 +238,7 @@ private struct DeployKeyAddSheet: View {
222238 @State private var text = ""
223239 @State private var canPush = false
224240 @State private var ttl: String?
241 @State private var attempted = false
225242
226243 var body: some View {
227244 NavigationStack {
@@ -246,7 +263,7 @@ private struct DeployKeyAddSheet: View {
246263 Text("In a year").tag(String?.some("365d"))
247264 }
248265 }
249 if let error = model.actionError {
266 if attempted, let error = model.actionError {
250267 Section { GBNotice(error, .gbWarn) }
251268 }
252269 }
@@ -261,7 +278,10 @@ private struct DeployKeyAddSheet: View {
261278 ProgressView()
262279 } else {
263280 Button("Add") {
264 Task { if await model.add(publicKey: text, canPush: canPush, ttl: ttl) { onDone() } }
281 Task {
282 attempted = true
283 if await model.add(publicKey: text, canPush: canPush, ttl: ttl) { onDone() }
284 }
265285 }
266286 .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
267287 .accessibilityIdentifier("deploy-key-submit")
gitbayTests/RepoCredentialTests.swift +24
@@ -71,6 +71,30 @@ struct BuildSecretsViewModelTests {
7171 #expect(model.actionError == "secret names are env-var shaped: uppercase letters, digits, _")
7272 }
7373
74 @Test func aFailedWriteStillRefreshesTheList() async throws {
75 let (client, stub) = try makeClient()
76 stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no secret NPM_TOKEN on krz/gitbay","exit_code":3}"#, match: "cmd"))
77 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":["DEPLOY_TOKEN"],"exit_code":0}"#, match: "argv=list"))
78 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
79
80 await model.remove("NPM_TOKEN")
81
82 #expect(model.actionError == "no secret NPM_TOKEN on krz/gitbay")
83 #expect(model.state.value == ["DEPLOY_TOKEN"])
84 }
85
86 @Test func aMultiLineValueKeepsItsNewlines() async throws {
87 let (client, stub) = try makeClient()
88 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
89 stub.enqueue(.init(status: 200, json: secretsJSON, match: "argv=list"))
90 let model = BuildSecretsViewModel(client: client, repoPath: "krz/gitbay")
91 let key = "-----BEGIN KEY-----\nabc\n-----END KEY-----\n"
92
93 #expect(await model.set(name: "BOT_SSH_KEY", value: key))
94
95 #expect(try body(try #require(stub.seen.first))["stdin"] as? String == key)
96 }
97
7498 @Test func removeNamesTheSecret() async throws {
7599 let (client, stub) = try makeClient()
76100 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))