From d2651e0592176dc25d6613716cbc6599dd9f959b Mon Sep 17 00:00:00 2001 From: stack72 Date: Wed, 8 Apr 2026 02:31:39 +0100 Subject: [PATCH] feat: tag swamp-originated lab issues with source="swamp" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit swamp-club#375 added a `source` field to lab issues so each one can be attributed to its origin (web app, CLI, extensions, UAT). Both paths in this repo that create lab issues — the CLI's `swamp issue` command and the GitHub auto-responder workflow — were defaulting to "swamp-club". Tag both with `source: "swamp"`. The auto-responder also switches from `/api/v1/lab/issues/ensure` to the public `/api/v1/lab/issues` endpoint, since `/ensure` doesn't accept the `source` field. The footer is expanded to include the full GitHub issue URL so the back-link survives the loss of the structured githubRepoFullName/githubIssueNumber fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/auto-response.yml | 29 +++++----- src/infrastructure/http/swamp_club_client.ts | 10 +++- .../http/swamp_club_client_test.ts | 53 ++++++++++++++++++- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index b91cf829..733a6c4b 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -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) { @@ -30,15 +29,19 @@ 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: { @@ -46,26 +49,24 @@ jobs: "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. @@ -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) }`, ); diff --git a/src/infrastructure/http/swamp_club_client.ts b/src/infrastructure/http/swamp_club_client.ts index ac74efe6..c47af9ed 100644 --- a/src/infrastructure/http/swamp_club_client.ts +++ b/src/infrastructure/http/swamp_club_client.ts @@ -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, @@ -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) { diff --git a/src/infrastructure/http/swamp_club_client_test.ts b/src/infrastructure/http/swamp_club_client_test.ts index 299b4348..8df35fd2 100644 --- a/src/infrastructure/http/swamp_club_client_test.ts +++ b/src/infrastructure/http/swamp_club_client_test.ts @@ -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, ): { port: number; shutdown: () => Promise } { const ac = new AbortController(); const server = Deno.serve( @@ -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 = {}; + 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(); + } +});