Commit d7fac80c5f

d7fac80c5f39e46eb7614e23711bbed89355757f

parent: ef8d699532

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 02:49 UTC

Send the webhook secret on stdin

webhook add refuses `--secret <value>` since krz/gitbay#284. Send
`--secret -` and put the secret on stdin.

Closes #21

Layout: unified · split

gitbay/Repos/WebhookViewModels.swift +6 −5
@@ -28,17 +28,18 @@ final class WebhookListViewModel {
28 } 28 }
29 } 29 }
30 30
31 /// `webhook add <repo> <url> [--secret <s>] [--events <e>]` — an empty 31 /// `webhook add <repo> <url> [--secret -] [--events <e>]` — an empty
32 /// secret or event list is left out, so the server applies its default. 32 /// secret or event list is left out, so the server applies its default.
33 /// The secret travels on stdin; the server refuses it in argv.
33 func add(url: String, secret: String, events: String) async -> Bool { 34 func add(url: String, secret: String, events: String) async -> Bool {
34 let url = url.trimmingCharacters(in: .whitespaces) 35 let url = url.trimmingCharacters(in: .whitespaces)
35 guard !url.isEmpty else { return false } 36 guard !url.isEmpty else { return false }
36 var argv = ["webhook", "add", repoPath, url] 37 var argv = ["webhook", "add", repoPath, url]
37 let secret = secret.trimmingCharacters(in: .whitespaces) 38 let secret = secret.trimmingCharacters(in: .whitespaces)
38 if !secret.isEmpty { argv += ["--secret", secret] } 39 if !secret.isEmpty { argv += ["--secret", "-"] }
39 let events = events.trimmingCharacters(in: .whitespaces) 40 let events = events.trimmingCharacters(in: .whitespaces)
40 if !events.isEmpty { argv += ["--events", events] } 41 if !events.isEmpty { argv += ["--events", events] }
41 await perform(argv) 42 await perform(argv, stdin: secret.isEmpty ? nil : secret + "\n")
42 return actionError == nil 43 return actionError == nil
43 } 44 }
44 45
@@ -46,12 +47,12 @@ final class WebhookListViewModel {
46 await perform(["webhook", "remove", repoPath, String(id)]) 47 await perform(["webhook", "remove", repoPath, String(id)])
47 } 48 }
48 49
49 private func perform(_ argv: [String]) async { 50 private func perform(_ argv: [String], stdin: String? = nil) async {
50 working = true 51 working = true
51 actionError = nil 52 actionError = nil
52 defer { working = false } 53 defer { working = false }
53 do { 54 do {
54 try await client.run(argv) 55 try await client.run(argv, stdin: stdin)
55 await load() 56 await load()
56 } catch let error as GitbayError { 57 } catch let error as GitbayError {
57 actionError = error.userFacingMessage 58 actionError = error.userFacingMessage
gitbayTests/WebhookTests.swift +8 −1
@@ -17,6 +17,11 @@ private func argvOf(_ seen: StubProtocol.Seen) throws -> [String] {
17 return try #require(body["argv"] as? [String]) 17 return try #require(body["argv"] as? [String])
18} 18}
19 19
20private func stdinOf(_ seen: StubProtocol.Seen) throws -> String? {
21 let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
22 return body["stdin"] as? String
23}
24
20private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# 25private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
21 26
22private let hooksJSON = """ 27private let hooksJSON = """
@@ -81,9 +86,11 @@ struct WebhookListViewModelTests {
81 #expect(first && second) 86 #expect(first && second)
82 let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf) 87 let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf)
83 #expect(writes == [ 88 #expect(writes == [
84 ["webhook", "add", "krz/gitbay", "https://a.example/h", "--secret", "s3", "--events", "push"], 89 ["webhook", "add", "krz/gitbay", "https://a.example/h", "--secret", "-", "--events", "push"],
85 ["webhook", "add", "krz/gitbay", "https://b.example/h"], 90 ["webhook", "add", "krz/gitbay", "https://b.example/h"],
86 ]) 91 ])
92 let posts = stub.seen.filter { $0.method == "POST" }
93 #expect(try posts.map(stdinOf) == ["s3\n", nil])
87 } 94 }
88 95
89 @Test func removeSendsTheIdThenReloads() async throws { 96 @Test func removeSendsTheIdThenReloads() async throws {