This commit is contained in:
parent
816881c2cc
commit
3379abf99b
9 changed files with 57 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# git-webhooks
|
# patchbay
|
||||||
|
|
||||||
Containerized Bun service for GitHub and jojo.build webhooks.
|
Containerized Bun service for GitHub and jojo.build webhooks.
|
||||||
|
|
||||||
|
|
@ -6,10 +6,13 @@ Containerized Bun service for GitHub and jojo.build webhooks.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GET /healthz
|
GET /healthz
|
||||||
POST /git-webhooks/jojo
|
POST /patchbay/jojo
|
||||||
POST /git-webhooks/github
|
POST /patchbay/github
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Existing `/git-webhooks/jojo` and `/git-webhooks/github` routes remain
|
||||||
|
compatibility aliases for existing webhook registrations.
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
|
||||||
2
bun.lock
2
bun.lock
|
|
@ -3,7 +3,7 @@
|
||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@peezy.tech/git-webhooks",
|
"name": "@peezy.tech/patchbay",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@peezy.tech/git-webhooks",
|
"name": "@peezy.tech/patchbay",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ export function buildDiscordPayload(input: DiscordNotification): DiscordPayload
|
||||||
].filter((item): item is DiscordEmbedField => item !== null);
|
].filter((item): item is DiscordEmbedField => item !== null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username: "git-webhooks",
|
username: "patchbay",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: feedTitle(signal).slice(0, 256),
|
title: feedTitle(signal).slice(0, 256),
|
||||||
|
|
@ -166,7 +166,7 @@ export function buildDiscordPayload(input: DiscordNotification): DiscordPayload
|
||||||
].filter((item): item is DiscordEmbedField => item !== null);
|
].filter((item): item is DiscordEmbedField => item !== null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username: "git-webhooks",
|
username: "patchbay",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: eventTitle(event).slice(0, 256),
|
title: eventTitle(event).slice(0, 256),
|
||||||
|
|
@ -175,7 +175,7 @@ export function buildDiscordPayload(input: DiscordNotification): DiscordPayload
|
||||||
fields,
|
fields,
|
||||||
timestamp: event.receivedAt,
|
timestamp: event.receivedAt,
|
||||||
footer: {
|
footer: {
|
||||||
text: "git-webhooks",
|
text: "patchbay",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,10 @@ export function createHandler(config: ServerConfig): (request: Request) => Promi
|
||||||
if (url.pathname === "/healthz") {
|
if (url.pathname === "/healthz") {
|
||||||
return textResponse("ok\n");
|
return textResponse("ok\n");
|
||||||
}
|
}
|
||||||
if (url.pathname === "/git-webhooks/github") {
|
if (url.pathname === "/patchbay/github" || url.pathname === "/git-webhooks/github") {
|
||||||
return handleGithub(request, config, store);
|
return handleGithub(request, config, store);
|
||||||
}
|
}
|
||||||
if (url.pathname === "/git-webhooks/jojo") {
|
if (url.pathname === "/patchbay/jojo" || url.pathname === "/git-webhooks/jojo") {
|
||||||
return handleJojo(request, config, store);
|
return handleJojo(request, config, store);
|
||||||
}
|
}
|
||||||
return jsonResponse({ error: "not_found" }, { status: 404 });
|
return jsonResponse({ error: "not_found" }, { status: 404 });
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ const pushEvent: GitWebhookEvent = {
|
||||||
receivedAt: "2026-05-12T21:00:00.000Z",
|
receivedAt: "2026-05-12T21:00:00.000Z",
|
||||||
repo: {
|
repo: {
|
||||||
owner: "peezy-tech",
|
owner: "peezy-tech",
|
||||||
name: "git-webhooks",
|
name: "patchbay",
|
||||||
fullName: "peezy-tech/git-webhooks",
|
fullName: "peezy-tech/patchbay",
|
||||||
},
|
},
|
||||||
sender: {
|
sender: {
|
||||||
username: "matamune",
|
username: "matamune",
|
||||||
|
|
@ -20,7 +20,7 @@ const pushEvent: GitWebhookEvent = {
|
||||||
after: "0123456789abcdef",
|
after: "0123456789abcdef",
|
||||||
raw: {
|
raw: {
|
||||||
head_commit: {
|
head_commit: {
|
||||||
url: "https://jojo.build/peezy-tech/git-webhooks/commit/0123456789abcdef",
|
url: "https://jojo.build/peezy-tech/patchbay/commit/0123456789abcdef",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -68,7 +68,7 @@ describe("discord notifications", () => {
|
||||||
id: "jojo:delivery-1:main_push",
|
id: "jojo:delivery-1:main_push",
|
||||||
kind: "main_push",
|
kind: "main_push",
|
||||||
provider: "jojo",
|
provider: "jojo",
|
||||||
repoFullName: "peezy-tech/git-webhooks",
|
repoFullName: "peezy-tech/patchbay",
|
||||||
ref: "refs/heads/main",
|
ref: "refs/heads/main",
|
||||||
sha: "0123456789abcdef",
|
sha: "0123456789abcdef",
|
||||||
deliveryId: "delivery-1",
|
deliveryId: "delivery-1",
|
||||||
|
|
@ -76,9 +76,9 @@ describe("discord notifications", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(payload.username).toBe("git-webhooks");
|
expect(payload.username).toBe("patchbay");
|
||||||
expect(payload.embeds[0].title).toBe("[jojo] peezy-tech/git-webhooks push to main");
|
expect(payload.embeds[0].title).toBe("[jojo] peezy-tech/patchbay push to main");
|
||||||
expect(payload.embeds[0].url).toBe("https://jojo.build/peezy-tech/git-webhooks/commit/0123456789abcdef");
|
expect(payload.embeds[0].url).toBe("https://jojo.build/peezy-tech/patchbay/commit/0123456789abcdef");
|
||||||
expect(payload.embeds[0].fields).toContainEqual({ name: "Queued", value: "main_push", inline: true });
|
expect(payload.embeds[0].fields).toContainEqual({ name: "Queued", value: "main_push", inline: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ describe("discord notifications", () => {
|
||||||
return new Response(null, { status: 204 });
|
return new Response(null, { status: 204 });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(JSON.parse(body).embeds[0].title).toBe("[jojo] peezy-tech/git-webhooks push to main");
|
expect(JSON.parse(body).embeds[0].title).toBe("[jojo] peezy-tech/patchbay push to main");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws on Discord failure", async () => {
|
test("throws on Discord failure", async () => {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ describe("feed watcher", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("first poll primes state without emitting old entries", async () => {
|
test("first poll primes state without emitting old entries", async () => {
|
||||||
const dataDir = await mkdtemp(join(tmpdir(), "git-webhooks-feed-"));
|
const dataDir = await mkdtemp(join(tmpdir(), "patchbay-feed-"));
|
||||||
const sourcesPath = join(dataDir, "sources.json");
|
const sourcesPath = join(dataDir, "sources.json");
|
||||||
await writeFile(sourcesPath, JSON.stringify({ sources: [source] }), "utf8");
|
await writeFile(sourcesPath, JSON.stringify({ sources: [source] }), "utf8");
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ describe("feed watcher", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("later polls emit new entries and release fork sync jobs", async () => {
|
test("later polls emit new entries and release fork sync jobs", async () => {
|
||||||
const dataDir = await mkdtemp(join(tmpdir(), "git-webhooks-feed-"));
|
const dataDir = await mkdtemp(join(tmpdir(), "patchbay-feed-"));
|
||||||
const sourcesPath = join(dataDir, "sources.json");
|
const sourcesPath = join(dataDir, "sources.json");
|
||||||
const releaseSource: FeedSourceConfig = {
|
const releaseSource: FeedSourceConfig = {
|
||||||
...source,
|
...source,
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { normalizeGithubEvent } from "../src/providers/github";
|
||||||
import { normalizeJojoEvent } from "../src/providers/jojo";
|
import { normalizeJojoEvent } from "../src/providers/jojo";
|
||||||
|
|
||||||
const repository = {
|
const repository = {
|
||||||
name: "git-webhooks",
|
name: "patchbay",
|
||||||
full_name: "peezy-tech/git-webhooks",
|
full_name: "peezy-tech/patchbay",
|
||||||
clone_url: "https://example.test/peezy-tech/git-webhooks.git",
|
clone_url: "https://example.test/peezy-tech/patchbay.git",
|
||||||
ssh_url: "git@example.test:peezy-tech/git-webhooks.git",
|
ssh_url: "git@example.test:peezy-tech/patchbay.git",
|
||||||
default_branch: "main",
|
default_branch: "main",
|
||||||
owner: { login: "peezy-tech" },
|
owner: { login: "peezy-tech" },
|
||||||
};
|
};
|
||||||
|
|
@ -28,7 +28,7 @@ describe("provider normalization", () => {
|
||||||
|
|
||||||
expect(event.provider).toBe("github");
|
expect(event.provider).toBe("github");
|
||||||
expect(event.event).toBe("push");
|
expect(event.event).toBe("push");
|
||||||
expect(event.repo?.fullName).toBe("peezy-tech/git-webhooks");
|
expect(event.repo?.fullName).toBe("peezy-tech/patchbay");
|
||||||
expect(event.sender?.username).toBe("peezy");
|
expect(event.sender?.username).toBe("peezy");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,15 @@ async function signedRequest(path: string, provider: "github" | "jojo", secret:
|
||||||
|
|
||||||
describe("server", () => {
|
describe("server", () => {
|
||||||
test("healthz returns ok", async () => {
|
test("healthz returns ok", async () => {
|
||||||
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir: await mkdtemp(join(tmpdir(), "git-webhooks-")) });
|
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir: await mkdtemp(join(tmpdir(), "patchbay-")) });
|
||||||
const response = await handler(new Request("http://localhost/healthz"));
|
const response = await handler(new Request("http://localhost/healthz"));
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(await response.text()).toBe("ok\n");
|
expect(await response.text()).toBe("ok\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects invalid signatures", async () => {
|
test("rejects invalid signatures", async () => {
|
||||||
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir: await mkdtemp(join(tmpdir(), "git-webhooks-")) });
|
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir: await mkdtemp(join(tmpdir(), "patchbay-")) });
|
||||||
const response = await handler(new Request("http://localhost/git-webhooks/github", {
|
const response = await handler(new Request("http://localhost/patchbay/github", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "x-hub-signature-256": "sha256=bad" },
|
headers: { "x-hub-signature-256": "sha256=bad" },
|
||||||
body: "{}",
|
body: "{}",
|
||||||
|
|
@ -40,14 +40,14 @@ describe("server", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("accepts jojo main pushes and queues a job", async () => {
|
test("accepts jojo main pushes and queues a job", async () => {
|
||||||
const dataDir = await mkdtemp(join(tmpdir(), "git-webhooks-"));
|
const dataDir = await mkdtemp(join(tmpdir(), "patchbay-"));
|
||||||
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir });
|
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir });
|
||||||
const request = await signedRequest("/git-webhooks/jojo", "jojo", "jojo", {
|
const request = await signedRequest("/patchbay/jojo", "jojo", "jojo", {
|
||||||
ref: "refs/heads/main",
|
ref: "refs/heads/main",
|
||||||
after: "abc123",
|
after: "abc123",
|
||||||
repository: {
|
repository: {
|
||||||
name: "git-webhooks",
|
name: "patchbay",
|
||||||
full_name: "peezy-tech/git-webhooks",
|
full_name: "peezy-tech/patchbay",
|
||||||
owner: { username: "peezy-tech" },
|
owner: { username: "peezy-tech" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -58,12 +58,30 @@ describe("server", () => {
|
||||||
expect(await readFile(join(dataDir, "jobs.jsonl"), "utf8")).toContain("\"kind\":\"main_push\"");
|
expect(await readFile(join(dataDir, "jobs.jsonl"), "utf8")).toContain("\"kind\":\"main_push\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps legacy git-webhooks routes as aliases", async () => {
|
||||||
|
const dataDir = await mkdtemp(join(tmpdir(), "patchbay-"));
|
||||||
|
const handler = createHandler({ githubSecret: "gh", jojoSecret: "jojo", dataDir });
|
||||||
|
const request = await signedRequest("/git-webhooks/jojo", "jojo", "jojo", {
|
||||||
|
ref: "refs/heads/main",
|
||||||
|
after: "abc123",
|
||||||
|
repository: {
|
||||||
|
name: "patchbay",
|
||||||
|
full_name: "peezy-tech/patchbay",
|
||||||
|
owner: { username: "peezy-tech" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await handler(request);
|
||||||
|
expect(response.status).toBe(202);
|
||||||
|
expect(await readFile(join(dataDir, "events.jsonl"), "utf8")).toContain("\"provider\":\"jojo\"");
|
||||||
|
});
|
||||||
|
|
||||||
test("continues accepting webhooks when Discord returns an error", async () => {
|
test("continues accepting webhooks when Discord returns an error", async () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
globalThis.fetch = (async () => new Response("bad", { status: 500 })) as unknown as typeof fetch;
|
globalThis.fetch = (async () => new Response("bad", { status: 500 })) as unknown as typeof fetch;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataDir = await mkdtemp(join(tmpdir(), "git-webhooks-"));
|
const dataDir = await mkdtemp(join(tmpdir(), "patchbay-"));
|
||||||
const handler = createHandler({
|
const handler = createHandler({
|
||||||
githubSecret: "gh",
|
githubSecret: "gh",
|
||||||
jojoSecret: "jojo",
|
jojoSecret: "jojo",
|
||||||
|
|
@ -73,12 +91,12 @@ describe("server", () => {
|
||||||
notifyEvents: new Set(["push"]),
|
notifyEvents: new Set(["push"]),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const request = await signedRequest("/git-webhooks/jojo", "jojo", "jojo", {
|
const request = await signedRequest("/patchbay/jojo", "jojo", "jojo", {
|
||||||
ref: "refs/heads/main",
|
ref: "refs/heads/main",
|
||||||
after: "abc123",
|
after: "abc123",
|
||||||
repository: {
|
repository: {
|
||||||
name: "git-webhooks",
|
name: "patchbay",
|
||||||
full_name: "peezy-tech/git-webhooks",
|
full_name: "peezy-tech/patchbay",
|
||||||
owner: { username: "peezy-tech" },
|
owner: { username: "peezy-tech" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue