From 0f319dfb807178d7923e6d20e5a27cf56e8343fe Mon Sep 17 00:00:00 2001 From: Wojtek Wrotek Date: Fri, 8 May 2026 09:17:28 +0200 Subject: [PATCH] Revert "feat/jira-app" --- .env.example | 9 -- env.ts | 7 -- src/adapters/issue-tracker/jira.test.ts | 105 ------------------------ src/adapters/issue-tracker/jira.ts | 65 +++------------ src/lib/adapters.ts | 2 - src/lib/step-adapters.ts | 2 - src/routes/jira/dispatch.post.ts | 67 --------------- 7 files changed, 13 insertions(+), 244 deletions(-) delete mode 100644 src/routes/jira/dispatch.post.ts diff --git a/.env.example b/.env.example index b8d55a9..343b006 100644 --- a/.env.example +++ b/.env.example @@ -13,15 +13,6 @@ COLUMN_BACKLOG=Backlog # Without this, dispatch falls back to ~60s cron polling on every ticket. JIRA_WEBHOOK_SECRET= -# Forge bridge (alternative to JIRA_WEBHOOK_SECRET; replaces the personal -# API-token comment author with the ai-workflow-jira-app Forge app user). -# Set both when running the companion ai-workflow-jira-app: -# - FORGE_SHARED_SECRET: same value passed to `forge variables set --encrypt SHARED_SECRET ...` -# - FORGE_COMMENT_URL: the `post-comment` URL printed by `forge webtrigger` -# When unset, /jira/dispatch returns 503 and postComment uses Basic auth. -FORGE_SHARED_SECRET= -FORGE_COMMENT_URL= - # VCS — choose one provider by setting VCS_KIND to "github" or "gitlab". # Only ONE VCS_KIND line should be active in this file. VCS_KIND=github diff --git a/env.ts b/env.ts index 9408d02..51516cd 100644 --- a/env.ts +++ b/env.ts @@ -116,13 +116,6 @@ export const env = createEnv({ // Jira Webhook JIRA_WEBHOOK_SECRET: z.string().min(1).optional(), - // Forge bridge — when both are set, /jira/dispatch authenticates the - // X-Forge-Secret header and JiraAdapter.postComment routes through the - // ai-workflow-jira-app Forge web trigger so comments are authored by - // the Forge app user instead of a personal Atlassian account. - FORGE_SHARED_SECRET: z.string().min(1).optional(), - FORGE_COMMENT_URL: z.string().url().optional(), - // Redis (run registry) AI_WORKFLOW_KV_REST_API_URL: z.string().url(), AI_WORKFLOW_KV_REST_API_TOKEN: z.string().min(1), diff --git a/src/adapters/issue-tracker/jira.test.ts b/src/adapters/issue-tracker/jira.test.ts index c197b77..4df2808 100644 --- a/src/adapters/issue-tracker/jira.test.ts +++ b/src/adapters/issue-tracker/jira.test.ts @@ -493,109 +493,4 @@ describe("JiraAdapter", () => { expect(collectText(body.body)).not.toContain("\n"); }); }); - - describe("postComment via Forge bridge", () => { - function forgeAdapter() { - return new JiraAdapter({ - baseUrl: "https://test.atlassian.net", - email: "test@example.com", - apiToken: "token", - projectKey: "PROJ", - forgeCommentUrl: "https://forge.example/x/post-comment", - forgeSharedSecret: "shh", - }); - } - - it("POSTs to the Forge web trigger with shared-secret header and plain-text body", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: "OK", - json: async () => ({ id: "55555", permalinkPath: "?focusedCommentId=55555" }), - }); - - const url = await forgeAdapter().postComment("PROJ-1", "hello\nworld"); - - expect(mockFetch).toHaveBeenCalledTimes(1); - const [calledUrl, init] = mockFetch.mock.calls[0]; - expect(calledUrl).toBe("https://forge.example/x/post-comment"); - const headers = init.headers as Record; - expect(headers["x-shared-secret"]).toBe("shh"); - expect(headers["Content-Type"]).toBe("application/json"); - expect(JSON.parse(init.body)).toEqual({ - issueKey: "PROJ-1", - body: "hello\nworld", - }); - expect(url).toBe( - "https://test.atlassian.net/browse/PROJ-1?focusedCommentId=55555", - ); - }); - - it("returns null when Forge response omits id or permalinkPath", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: "OK", - json: async () => ({ id: null, permalinkPath: null }), - }); - - const url = await forgeAdapter().postComment("PROJ-1", "hi"); - expect(url).toBeNull(); - }); - - it("maps 404 from Forge to IssueTrackerNotFoundError", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: "Not Found", - json: async () => ({ error: "jira_error", status: 404 }), - }); - - await expect(forgeAdapter().postComment("PROJ-9", "x")).rejects.toBeInstanceOf( - IssueTrackerNotFoundError, - ); - }); - - it("throws on other non-2xx Forge responses", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 502, - statusText: "Bad Gateway", - json: async () => ({ error: "jira_error", status: 500 }), - }); - - await expect(forgeAdapter().postComment("PROJ-1", "x")).rejects.toThrow( - /Forge postComment error: 502/, - ); - }); - - it("falls back to direct Basic-auth path when only one Forge field is set", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ id: "98765" }), - }); - - const adapter = new JiraAdapter({ - baseUrl: "https://test.atlassian.net", - email: "test@example.com", - apiToken: "token", - projectKey: "PROJ", - forgeCommentUrl: "https://forge.example/x/post-comment", - // forgeSharedSecret intentionally omitted - }); - - const url = await adapter.postComment("PROJ-1", "hi"); - - const [calledUrl, init] = mockFetch.mock.calls[0]; - expect(calledUrl).toBe( - "https://test.atlassian.net/rest/api/3/issue/PROJ-1/comment", - ); - expect((init.headers as Record).Authorization).toMatch( - /^Basic /, - ); - expect(url).toBe( - "https://test.atlassian.net/browse/PROJ-1?focusedCommentId=98765", - ); - }); - }); }); diff --git a/src/adapters/issue-tracker/jira.ts b/src/adapters/issue-tracker/jira.ts index b4021a0..30b1a9a 100644 --- a/src/adapters/issue-tracker/jira.ts +++ b/src/adapters/issue-tracker/jira.ts @@ -11,8 +11,6 @@ export interface JiraConfig { email: string; apiToken: string; projectKey: string; - forgeCommentUrl?: string; - forgeSharedSecret?: string; } export class JiraAdapter implements IssueTrackerAdapter { @@ -41,9 +39,7 @@ export class JiraAdapter implements IssueTrackerAdapter { if (res.status === 404) { throw new IssueTrackerNotFoundError("Jira resource", path); } - throw new Error( - `Jira API error: ${res.status} ${res.statusText} on ${path}`, - ); + throw new Error(`Jira API error: ${res.status} ${res.statusText} on ${path}`); } if (res.status === 204) return null; try { @@ -73,19 +69,17 @@ export class JiraAdapter implements IssueTrackerAdapter { ), labels: data.fields.labels ?? [], trackerStatus: data.fields.status?.name ?? "", - attachments: (data.fields.attachment ?? []).map( - (a: any): TicketAttachment => { - const contentUrl = - a.content == null ? undefined : String(a.content).trim(); - return { - id: String(a.id), - filename: a.filename ?? "", - mimeType: a.mimeType ?? "application/octet-stream", - size: sanitizeAttachmentSize(a.size), - contentUrl: contentUrl || undefined, - }; - }, - ), + attachments: (data.fields.attachment ?? []).map((a: any): TicketAttachment => { + const contentUrl = + a.content == null ? undefined : String(a.content).trim(); + return { + id: String(a.id), + filename: a.filename ?? "", + mimeType: a.mimeType ?? "application/octet-stream", + size: sanitizeAttachmentSize(a.size), + contentUrl: contentUrl || undefined, + }; + }), }; } @@ -106,9 +100,6 @@ export class JiraAdapter implements IssueTrackerAdapter { } async postComment(id: string, comment: string): Promise { - if (this.config.forgeCommentUrl && this.config.forgeSharedSecret) { - return this.postCommentViaForge(id, comment); - } const data = await this.request(`/rest/api/3/issue/${id}/comment`, { method: "POST", body: JSON.stringify({ @@ -124,34 +115,6 @@ export class JiraAdapter implements IssueTrackerAdapter { return `${this.baseUrl}/browse/${encodeURIComponent(id)}?focusedCommentId=${encodeURIComponent(commentId)}`; } - private async postCommentViaForge( - id: string, - comment: string, - ): Promise { - const res = await fetch(this.config.forgeCommentUrl!, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-shared-secret": this.config.forgeSharedSecret!, - }, - body: JSON.stringify({ issueKey: id, body: comment }), - }); - if (res.status === 404) { - throw new IssueTrackerNotFoundError("Jira issue", id); - } - if (!res.ok) { - throw new Error( - `Forge postComment error: ${res.status} ${res.statusText}`, - ); - } - const data = (await res.json()) as { - id?: string | null; - permalinkPath?: string | null; - }; - if (!data?.id || !data?.permalinkPath) return null; - return `${this.baseUrl}/browse/${encodeURIComponent(id)}${data.permalinkPath}`; - } - async downloadAttachment( url: string, opts: { timeoutMs?: number } = {}, @@ -238,9 +201,7 @@ function extractAdfText(adf: any): string { function extractAcceptanceCriteria(description: any): string { const text = extractAdfText(description); - const match = text.match( - /acceptance criteria[:\s]*([\s\S]*?)(?:\n\n|\n#|$)/i, - ); + const match = text.match(/acceptance criteria[:\s]*([\s\S]*?)(?:\n\n|\n#|$)/i); return match?.[1]?.trim() ?? ""; } diff --git a/src/lib/adapters.ts b/src/lib/adapters.ts index 16597b9..73d9a75 100644 --- a/src/lib/adapters.ts +++ b/src/lib/adapters.ts @@ -40,8 +40,6 @@ export function createAdapters(): Adapters { email: env.JIRA_EMAIL, apiToken: env.JIRA_API_TOKEN, projectKey: env.JIRA_PROJECT_KEY, - forgeCommentUrl: env.FORGE_COMMENT_URL, - forgeSharedSecret: env.FORGE_SHARED_SECRET, }), vcs: createVCS(), messaging, diff --git a/src/lib/step-adapters.ts b/src/lib/step-adapters.ts index 2cc89d1..9643779 100644 --- a/src/lib/step-adapters.ts +++ b/src/lib/step-adapters.ts @@ -37,8 +37,6 @@ export function createStepAdapters(): StepAdapters { email: env.JIRA_EMAIL, apiToken: env.JIRA_API_TOKEN, projectKey: env.JIRA_PROJECT_KEY, - forgeCommentUrl: env.FORGE_COMMENT_URL, - forgeSharedSecret: env.FORGE_SHARED_SECRET, }), vcs: createVCS(), messaging, diff --git a/src/routes/jira/dispatch.post.ts b/src/routes/jira/dispatch.post.ts deleted file mode 100644 index 05154ff..0000000 --- a/src/routes/jira/dispatch.post.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { timingSafeEqual } from "node:crypto"; -import { defineEventHandler, readBody, getHeader, createError } from "h3"; -import { env } from "../../../env.js"; -import { createAdapters } from "../../lib/adapters.js"; -import { dispatchTicket } from "../../lib/dispatch.js"; -import { logger } from "../../lib/logger.js"; - -/** - * Forge bridge endpoint — receives forwarded Jira issue-updated events from - * the ai-workflow-jira-app Forge app. Authenticated by a shared secret in - * the X-Forge-Secret header instead of HMAC over the body (Forge handles the - * Jira-side auth via api.asApp(), so we only need to verify that the caller - * is our own Forge install). - * - * Coexists with /webhooks/jira: either path can drive dispatch during the - * cutover. After Forge is confirmed working, deregister the manual webhook - * in Jira admin UI. - */ -export default defineEventHandler(async (event) => { - if (!env.FORGE_SHARED_SECRET) { - throw createError({ statusCode: 503, statusMessage: "Forge bridge disabled" }); - } - - const provided = getHeader(event, "x-forge-secret") ?? ""; - const a = Buffer.from(provided); - const b = Buffer.from(env.FORGE_SHARED_SECRET); - if (a.length !== b.length || !timingSafeEqual(a, b)) { - throw createError({ statusCode: 401, statusMessage: "Invalid forge secret" }); - } - - const body = await readBody(event); - const ticketKey = typeof body?.issueKey === "string" ? body.issueKey.trim() : ""; - if (!ticketKey) { - return { status: "ignored", reason: "no_ticket_key" }; - } - - const expectedPrefix = `${env.JIRA_PROJECT_KEY.trim().toUpperCase()}-`; - if (!ticketKey.toUpperCase().startsWith(expectedPrefix)) { - logger.debug( - { ticketKey, expectedProject: env.JIRA_PROJECT_KEY }, - "forge_dispatch_ignored_wrong_project", - ); - return { status: "ignored", reason: "wrong_project", ticketKey }; - } - - logger.info( - { - ticketKey, - source: typeof body?.source === "string" ? body.source : "forge", - cloudId: typeof body?.cloudId === "string" ? body.cloudId : null, - payloadStatus: typeof body?.payloadStatus === "string" ? body.payloadStatus : null, - }, - "forge_dispatch_received", - ); - - const adapters = createAdapters(); - const result = await dispatchTicket(ticketKey, adapters, env.MAX_CONCURRENT_AGENTS); - logger.info( - { ticketKey, started: result.started, reason: result.reason, runId: result.runId }, - "forge_dispatch_result", - ); - return { - status: result.started ? "dispatched" : "skipped", - ticketKey, - reason: result.reason, - }; -});