Skip to content

Commit 8df3011

Browse files
committed
fix(wikis): pass If-Match ETag header on wiki page updates
The wiki page update code fetched the existing page to get the ETag but never passed it to the PUT call — the _existing binding discarded the response. Without the If-Match header, concurrent edits could silently overwrite each other. Changes: * lib/ado_cli/client.ex — added Client.put/4 that accepts extra HTTP headers (e.g. If-Match). The new 4-arity function passes extra_headers through do_request -> do_request_with_auth, where they're merged with auth headers. The existing 3-arity put/3 is unchanged. * lib/ado_cli/cli/wikis.ex — the update_page function now extracts the eTag from the fetched page and passes it as {"If-Match", etag} to Client.put/4. If the page has no ETag (unlikely), the PUT proceeds without it. * lib/ado_cli/cli/test_results.ex — replaced Jason.encode!/1 with JSON.encode!/1 (Elixir 1.20 built-in) to silence a dialyzer warning. 325 tests pass, dialyzer clean, credo clean.
1 parent 40e7d1c commit 8df3011

3 files changed

Lines changed: 25 additions & 13 deletions

File tree

lib/ado_cli/cli/test_results.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ defmodule AdoCli.CLI.TestResults do
211211
json? = Map.get(parsed.options, :json, false)
212212

213213
if json? do
214-
writeln(Jason.encode!(%{ok: true, run: %{id: run["id"], name: name}}))
214+
writeln(JSON.encode!(%{ok: true, run: %{id: run["id"], name: name}}))
215215
else
216216
writeln("")
217217
writeln("✓ Test run ##{run["id"]} created: #{name}")

lib/ado_cli/cli/wikis.ex

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,23 @@ defmodule AdoCli.CLI.Wikis do
187187
path = Map.fetch!(parsed.options, :path)
188188
content = Map.fetch!(parsed.options, :content)
189189

190-
# Fetch existing etag
190+
# Fetch the existing page to get its ETag for optimistic
191+
# concurrency. The If-Match header prevents overwriting
192+
# changes made by another user between our read and write.
191193
case Client.get("/#{URI.encode(project)}/_apis/wiki/wikis/#{URI.encode(wiki_id)}/pages", %{
192-
"path" => path
194+
"path" => path,
195+
"includeContent" => "true"
193196
}) do
194-
{:ok, _existing} ->
197+
{:ok, existing} ->
198+
etag = existing["eTag"] || ""
199+
extra_headers = if etag != "", do: [{"If-Match", etag}], else: []
195200
body = %{"content" => content}
196201

197202
case Client.put(
198203
"/#{URI.encode(project)}/_apis/wiki/wikis/#{URI.encode(wiki_id)}/pages",
199204
body,
200-
%{"path" => path, "comment" => "Updated via ado CLI"}
205+
%{"path" => path, "comment" => "Updated via ado CLI"},
206+
extra_headers
201207
) do
202208
{:ok, page} ->
203209
success("Page '#{page["path"]}' updated.\n")

lib/ado_cli/client.ex

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,14 @@ defmodule AdoCli.Client do
7070
@doc """
7171
Makes a PUT request.
7272
"""
73-
def put(path, body, params \\ %{}) do
74-
handle_response(do_request(:put, path, body, params))
73+
def put(path, body, params \\ %{}), do: put(path, body, params, [])
74+
75+
@doc """
76+
PUT with extra HTTP headers (e.g. If-Match for optimistic concurrency).
77+
`extra_headers` is merged with the default auth headers.
78+
"""
79+
def put(path, body, params, extra_headers) do
80+
handle_response(do_request(:put, path, body, params, extra_headers))
7581
end
7682

7783
# ── Private ──────────────────────────────────────────────────────────
@@ -95,21 +101,21 @@ defmodule AdoCli.Client do
95101

96102
defp safe_decode(body), do: body
97103

98-
defp do_request(method, path, body, params, attempt \\ 0)
104+
defp do_request(method, path, body, params, extra_headers \\ [], attempt \\ 0)
99105

100-
defp do_request(_method, _path, _body, _params, 3),
106+
defp do_request(_method, _path, _body, _params, _extra_headers, 3),
101107
do: {:error, %{status: 302, body: "Too many redirects"}}
102108

103-
defp do_request(method, path, body, params, attempt) do
109+
defp do_request(method, path, body, params, extra_headers, attempt) do
104110
with {:ok, org, auth_headers} <- AdoCli.Auth.resolve_auth() do
105-
do_request_with_auth(method, path, body, params, attempt, org, auth_headers)
111+
do_request_with_auth(method, path, body, params, attempt, org, auth_headers, extra_headers)
106112
end
107113
end
108114

109-
defp do_request_with_auth(method, path, body, params, attempt, org, auth_headers) do
115+
defp do_request_with_auth(method, path, body, params, attempt, org, auth_headers, extra_headers) do
110116
url = build_url(path, params)
111117
full_url = inject_org(url, org)
112-
headers = [{"Content-Type", "application/json"} | auth_headers]
118+
headers = [{"Content-Type", "application/json"} | auth_headers] ++ extra_headers
113119
encoded = if body, do: JSON.encode!(body)
114120

115121
case Finch.request(Finch.build(method, full_url, headers, encoded), AdoCli.Finch) do

0 commit comments

Comments
 (0)