Packages webhook: read CloudEvent type from ce-type header

f795f88 · gmorell · 2026-06-25 21:43

1 files +49 -22
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud_web/controllers/packages_webhook_controller.ex
+49 −22
@@ -39,9 +39,13 @@ defmodule GitGudWeb.PackagesWebhookController do
39 39
40 40 def notify(conn, params) do
41 41 with :ok <- authenticate(conn) do
42 Logger.info("zot webhook: #{inspect(params, limit: 40, printable_limit: 1500)}")
42 + ce_type = cloudevent_type(conn)
43 43
44 case classify(params) do
44 + Logger.debug(fn ->
45 + "zot webhook: ce-type=#{inspect(ce_type)} name=#{inspect(params["name"])} ref=#{inspect(params["reference"])}"
46 + end)
47 +
48 + case classify(ce_type, params) do
45 49 {:push, payload} ->
46 50 handle_push(conn, payload)
47 51
@@ -59,17 +63,14 @@ defmodule GitGudWeb.PackagesWebhookController do
59 63
60 64 # ── dispatch ────────────────────────────────────────────────────────
61 65
62 # Old zot `notifications` payloads carried `event`/`type` = PUSH/DELETE
63 # at the top level; the v2.1 `events` extension sends a CloudEvent with
64 # `type` like `zotregistry.image.updated` and the image fields nested
65 # under `data`. Handle both: derive the action from whichever action
66 # string is present, and operate on the unwrapped data map.
67 defp classify(%{"event" => event} = p) when is_binary(event), do: classify_action(event, p)
68 defp classify(%{"type" => type} = p) when is_binary(type), do: classify_action(type, p)
69 defp classify(_), do: :ignored
70
71 defp classify_action(action, p) do
72 data = unwrap_data(p)
66 + # zot's v2.1 `events` extension sends CloudEvents in *binary* mode: the
67 + # event type (`zotregistry.image.updated` / `.deleted`) is in the
68 + # `ce-type` HTTP header, and the image fields (name/reference/digest/…)
69 + # are the JSON body. Older `notifications` payloads carried the action
70 + # as a body `event`/`type` field — fall back to that.
71 + defp classify(ce_type, params) do
72 + action = ce_type || params["type"] || params["event"] || ""
73 + data = unwrap_data(params)
73 74 a = String.downcase(action)
74 75
75 76 cond do
@@ -84,22 +85,34 @@ defmodule GitGudWeb.PackagesWebhookController do
84 85 end
85 86 end
86 87
88 + defp cloudevent_type(conn) do
89 + case get_req_header(conn, "ce-type") do
90 + [t | _] -> t
91 + _ -> nil
92 + end
93 + end
94 +
87 95 defp unwrap_data(%{"data" => %{} = data}), do: data
88 96 defp unwrap_data(p), do: p
89 97
90 98 defp handle_push(conn, p) do
91 99 with {:ok, repo, image} <- resolve_repo_and_image(repository(p)),
92 100 reference when is_binary(reference) <- reference(p) do
93 attrs = %{
94 version: reference,
95 digest: digest(p) || "",
96 size: parse_size(p),
97 media_type: p["mediaType"] || p["MediaType"],
98 manifest_doc: p["manifest"] || p["Manifest"] || %{},
99 pushed_at: parse_timestamp(p["timestamp"] || p["time"] || p["Timestamp"])
100 }
101 + # Skip by-digest manifest pushes (the per-arch children of an
102 + # image index) — only tags are worth surfacing.
103 + unless digest_ref?(reference) do
104 + attrs = %{
105 + version: reference,
106 + digest: digest(p) || "",
107 + size: parse_size(p),
108 + media_type: p["mediaType"] || p["MediaType"],
109 + manifest_doc: decode_manifest(p["manifest"] || p["Manifest"]),
110 + pushed_at: parse_timestamp(p["timestamp"] || p["time"] || p["Timestamp"])
111 + }
112 +
113 + _ = Packages.record_push(repo, "container", image, attrs)
114 + end
101 115
102 _ = Packages.record_push(repo, "container", image, attrs)
103 116 send_resp(conn, 204, "")
104 117 else
105 118 _ -> send_resp(conn, 422, "unparseable payload")
@@ -121,6 +134,20 @@ defmodule GitGudWeb.PackagesWebhookController do
121 134 defp reference(p), do: p["reference"] || p["tag"] || p["Reference"] || p["Tag"]
122 135 defp digest(p), do: p["digest"] || p["Digest"] || p["manifestDigest"]
123 136
137 + defp digest_ref?(ref), do: String.starts_with?(ref, "sha256:")
138 +
139 + # The events payload carries `manifest` as a JSON string; the schema
140 + # field is a map.
141 + defp decode_manifest(m) when is_binary(m) do
142 + case Jason.decode(m) do
143 + {:ok, map} when is_map(map) -> map
144 + _ -> %{}
145 + end
146 + end
147 +
148 + defp decode_manifest(m) when is_map(m), do: m
149 + defp decode_manifest(_), do: %{}
150 +
124 151 # ── helpers ────────────────────────────────────────────────────────
125 152
126 153 defp resolve_repo_and_image(nil), do: :error

Parents: 34ac246