fix(api,ops): transactional adopt, admin gate without a flash, and a digest path to deploys

The self-context adopt runs read-modify-write inside one transaction,
the admin shell renders nothing until the role is confirmed, and the
json-body helper merges Headers instances correctly. The compose image
reference accepts a pinned digest through an env var, and a promote
script resolves the published digest, verifies its signature and hands
the operator the exact pinning call — the documented path away from a
moving latest tag.
This commit is contained in:
Marc-André Bombeck committed 2026-06-11 11:45:58 +02:00
1 parent 599b7e8c99
commit f13aa684f9
12 files changed
+876 -79

No files matched your search

+46 -10
View File
@@ -192,11 +192,13 @@ describe("verb helpers", () => {
await helper("/api/foo", { a: 1 });
expect(fetchMock).toHaveBeenCalledWith("/api/foo", {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ a: 1 }),
});
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe("/api/foo");
expect(init.method).toBe(method);
expect(init.body).toBe(JSON.stringify({ a: 1 }));
expect(new Headers(init.headers).get("Content-Type")).toBe(
"application/json",
);
});
it("omits body and Content-Type when no body is given", async () => {
@@ -212,10 +214,44 @@ describe("verb helpers", () => {
await apiPost("/api/foo", { a: 1 }, { headers: { "X-Req": "1" } });
expect(fetchMock).toHaveBeenCalledWith("/api/foo", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Req": "1" },
body: JSON.stringify({ a: 1 }),
});
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const headers = new Headers(init.headers);
expect(headers.get("Content-Type")).toBe("application/json");
expect(headers.get("X-Req")).toBe("1");
expect(init.body).toBe(JSON.stringify({ a: 1 }));
});
it("keeps caller headers passed as a Headers instance (no spread loss)", async () => {
// A `Headers` instance has no enumerable own properties, so the
// previous `{ ...init.headers }` merge silently dropped every
// caller header for this `HeadersInit` shape.
fetchMock.mockResolvedValueOnce(jsonResponse({ data: null, error: null }));
await apiPost(
"/api/foo",
{ a: 1 },
{ headers: new Headers({ "X-Req": "1", "Idempotency-Key": "k1" }) },
);
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const headers = new Headers(init.headers);
expect(headers.get("X-Req")).toBe("1");
expect(headers.get("Idempotency-Key")).toBe("k1");
expect(headers.get("Content-Type")).toBe("application/json");
});
it("respects a caller-supplied Content-Type override", async () => {
fetchMock.mockResolvedValueOnce(jsonResponse({ data: null, error: null }));
await apiPost(
"/api/foo",
{ a: 1 },
{ headers: new Headers({ "Content-Type": "application/json-patch+json" }) },
);
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(new Headers(init.headers).get("Content-Type")).toBe(
"application/json-patch+json",
);
});
});
+9 -1
View File
@@ -141,7 +141,15 @@ function withJsonBody(
): RequestInit {
const merged: RequestInit = { ...init, method };
if (body !== undefined) {
merged.headers = { "Content-Type": "application/json", ...init?.headers };
// `HeadersInit` is a union — a plain object spreads fine, but a
// `Headers` instance (or entries array) has no enumerable own
// properties, so `{ ...init.headers }` silently dropped every
// caller header. `new Headers(...)` normalises all three shapes.
const headers = new Headers(init?.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
merged.headers = headers;
merged.body = JSON.stringify(body);
}
return merged;