Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions .github/workflows/auto-response.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issue = context.payload.issue;
const repo = `${context.repo.owner}/${context.repo.repo}`;

const apiKey = process.env.SWAMP_CLUB_API_KEY;
if (!apiKey) {
Expand All @@ -30,42 +29,44 @@ jobs:
}

// Build the lab issue body: original body + auto-move footer.
// The footer carries the link back to the GitHub issue since the
// public /issues endpoint doesn't store githubRepoFullName /
// githubIssueNumber on the lab issue itself.
const originalBody = (issue.body ?? "").trim();
const footer = `Automoved by swampadmin from GitHub issue #${issue.number}`;
const footer = `Automoved by swampadmin from ${issue.html_url}`;
const labBody = originalBody.length > 0
? `${originalBody}\n\n---\n${footer}`
: footer;

// Create (or fetch) the issue in the swamp.club lab.
const ensureRes = await fetch(
"https://swamp.club/api/v1/lab/issues/ensure",
// Create the lab issue via the public submission endpoint. The
// `source: "swamp"` tag attributes it to this repo in the Lab UI.
const createRes = await fetch(
"https://swamp.club/api/v1/lab/issues",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
githubRepoFullName: repo,
githubIssueNumber: issue.number,
source: "swamp",
type: "feature",
title: issue.title,
body: labBody,
type: "feature",
githubAuthorLogin: issue.user.login,
}),
signal: AbortSignal.timeout(15_000),
},
);

if (!ensureRes.ok) {
const text = await ensureRes.text().catch(() => "");
if (!createRes.ok) {
const text = await createRes.text().catch(() => "");
core.setFailed(
`swamp.club ensure failed: ${ensureRes.status} ${text}`,
`swamp.club create failed: ${createRes.status} ${text}`,
);
return;
}

const data = await ensureRes.json();
const data = await createRes.json();
// Lab issues use a sequential numeric id (LabIssueData.number) as
// the human-facing identifier — see swamp-club commit 9f12761c.
// The lab UI route and all API paths use /lab/{number} after #369.
Expand All @@ -78,7 +79,7 @@ jobs:
labIssueNumber <= 0
) {
core.setFailed(
`swamp.club ensure returned no lab issue number: ${
`swamp.club create returned no lab issue number: ${
JSON.stringify(data)
}`,
);
Expand Down
10 changes: 8 additions & 2 deletions src/infrastructure/http/swamp_club_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ export class SwampClubClient {

/**
* Submit a bug report or feature request to the Lab.
* Authenticates using the x-api-key header.
* Authenticates using the x-api-key header. Tags every submission with
* `source: "swamp"` so the Lab UI can attribute it to the CLI.
*/
async submitIssue(
apiKey: string,
Expand All @@ -176,7 +177,12 @@ export class SwampClubClient {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(input),
body: JSON.stringify({
source: "swamp",
type: input.type,
title: input.title,
body: input.body,
}),
});

if (!res.ok) {
Expand Down
53 changes: 52 additions & 1 deletion src/infrastructure/http/swamp_club_client_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { UserError } from "../../domain/errors.ts";

/** Start a simple mock HTTP server that returns canned responses. */
function startMockServer(
handler: (req: Request) => Response,
handler: (req: Request) => Response | Promise<Response>,
): { port: number; shutdown: () => Promise<void> } {
const ac = new AbortController();
const server = Deno.serve(
Expand Down Expand Up @@ -163,3 +163,54 @@ Deno.test("SwampClubClient - sends x-api-key header for whoami", async () => {
await mock.shutdown();
}
});

Deno.test("SwampClubClient - submitIssue posts source=swamp with input fields", async () => {
let capturedBody: Record<string, unknown> = {};
let capturedApiKey = "";
const mock = startMockServer(async (req) => {
capturedApiKey = req.headers.get("x-api-key") ?? "";
capturedBody = await req.json();
return Response.json(
{ issue: { number: 42, id: "issue-id-1" } },
{ status: 201 },
);
});
try {
const client = new SwampClubClient(`http://localhost:${mock.port}`);
const result = await client.submitIssue("my-api-key", {
type: "bug",
title: "Crash on launch",
body: "Repro steps...",
});
assertEquals(result.number, 42);
assertEquals(result.id, "issue-id-1");
assertEquals(capturedApiKey, "my-api-key");
assertEquals(capturedBody.source, "swamp");
assertEquals(capturedBody.type, "bug");
assertEquals(capturedBody.title, "Crash on launch");
assertEquals(capturedBody.body, "Repro steps...");
} finally {
await mock.shutdown();
}
});

Deno.test("SwampClubClient - submitIssue throws UserError on failure", async () => {
const mock = startMockServer((_req) =>
new Response("Bad request", { status: 400 })
);
try {
const client = new SwampClubClient(`http://localhost:${mock.port}`);
await assertRejects(
() =>
client.submitIssue("key", {
type: "feature",
title: "t",
body: "b",
}),
UserError,
"Failed to submit issue",
);
} finally {
await mock.shutdown();
}
});
Loading