From 907489e2815287d5c4f8e7f684b4588649a84c97 Mon Sep 17 00:00:00 2001 From: Ramesh Nethi Date: Mon, 27 Apr 2026 17:53:29 +0530 Subject: [PATCH] fix: align ticket provider credentials and fix settings status check Ensures GitHub ticket providers created via Settings or the new repository flow automatically resolve and store a dedicated credential secret. Also fixes the false-negative 'No token configured' on the Settings page by making the status check workspace-aware, and adds validation tests for the provider creation safety check. --- apps/api/src/routes/github-token.ts | 4 +-- .../routes/request-input-validation.test.ts | 14 +++++++- apps/api/src/routes/tickets.ts | 27 +++++++++++++- apps/web/src/app/repos/new/page.tsx | 36 +++++++++++++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/apps/api/src/routes/github-token.ts b/apps/api/src/routes/github-token.ts index 3a2a7b16..7ac8b58a 100644 --- a/apps/api/src/routes/github-token.ts +++ b/apps/api/src/routes/github-token.ts @@ -63,10 +63,10 @@ export async function githubTokenRoutes(rawApp: FastifyInstance) { response: { 200: GitHubTokenStatusResponseSchema }, }, }, - async (_req, reply) => { + async (req, reply) => { let token: string | null = null; try { - token = await retrieveSecret("GITHUB_TOKEN"); + token = await retrieveSecret("GITHUB_TOKEN", "global", req.user?.workspaceId); } catch { /* no token stored */ } diff --git a/apps/api/src/routes/request-input-validation.test.ts b/apps/api/src/routes/request-input-validation.test.ts index 00e6f37a..cfff855e 100644 --- a/apps/api/src/routes/request-input-validation.test.ts +++ b/apps/api/src/routes/request-input-validation.test.ts @@ -179,10 +179,22 @@ describe("tickets route body validation", () => { const res = await app.inject({ method: "POST", url: "/api/tickets/providers", - payload: { source: "github", config: { org: "test" }, enabled: true }, + payload: { source: "github", config: { token: "ghp_test", org: "test" }, enabled: true }, }); expect(res.statusCode).toBe(201); }); + + it("rejects POST /api/tickets/providers for GitHub when no token is available", async () => { + // In the test env, getGitHubToken will fail by default as no secrets/app are configured + const res = await app.inject({ + method: "POST", + url: "/api/tickets/providers", + payload: { source: "github", config: { org: "test" }, enabled: true }, + }); + expect(res.statusCode).toBe(400); + const body = JSON.parse(res.body); + expect(body.error).toContain("No GitHub token available"); + }); }); describe("pr-reviews route body validation", () => { diff --git a/apps/api/src/routes/tickets.ts b/apps/api/src/routes/tickets.ts index 1c80bb51..77b03249 100644 --- a/apps/api/src/routes/tickets.ts +++ b/apps/api/src/routes/tickets.ts @@ -15,6 +15,7 @@ import { HmacSha256Verifier } from "../services/crypto/signer.js"; import { logger } from "../logger.js"; import { ErrorResponseSchema, IdParamsSchema } from "../schemas/common.js"; import { TicketProviderSchema } from "../schemas/integration.js"; +import { getGitHubToken } from "../services/github-token-service.js"; // ── Zod schemas for ticket provider config ───────────────────────────────── @@ -212,7 +213,10 @@ export async function ticketRoutes(rawApp: FastifyInstance) { "linked to the provider record.", tags: ["Repos & Integrations"], body: ticketProviderConfigSchema, - response: { 201: ProviderResponseSchema }, + response: { + 201: ProviderResponseSchema, + 400: ErrorResponseSchema, + }, }, }, async (req, reply) => { @@ -229,6 +233,27 @@ export async function ticketRoutes(rawApp: FastifyInstance) { } } + // If this is a GitHub provider and no token was provided, attempt to resolve one. + // This aligns the Settings UI creation flow with the Setup Wizard flow. + if (body.source === "github" && !sensitiveValues.token) { + try { + const resolvedToken = await getGitHubToken({ + server: true, + workspaceId: req.user?.workspaceId, + }); + sensitiveValues.token = resolvedToken; + } catch (err) { + logger.warn( + { err, workspaceId: req.user?.workspaceId }, + "Failed to resolve GitHub token for new provider", + ); + return reply.status(400).send({ + error: + "No GitHub token available. Please configure a GitHub App or add a GITHUB_TOKEN secret first.", + }); + } + } + const [provider] = await db .insert(ticketProviders) .values({ diff --git a/apps/web/src/app/repos/new/page.tsx b/apps/web/src/app/repos/new/page.tsx index d5b1279d..75f61c8b 100644 --- a/apps/web/src/app/repos/new/page.tsx +++ b/apps/web/src/app/repos/new/page.tsx @@ -72,6 +72,7 @@ export default function NewRepoPage() { const [testCommand, setTestCommand] = useState(""); const [autoResume, setAutoResume] = useState(false); const [autoMerge, setAutoMerge] = useState(false); + const [ticketIntegrationEnabled, setTicketIntegrationEnabled] = useState(false); // Creating const [creating, setCreating] = useState(false); @@ -142,6 +143,16 @@ export default function NewRepoPage() { autoMerge, }); + if (ticketIntegrationEnabled) { + const [owner, repo] = fullName.split("/"); + if (owner && repo) { + await api.createTicketProvider({ + source: "github", + config: { owner, repo, label: "optio" }, + }); + } + } + toast.success(`${fullName} added successfully`); router.push(`/repos/${repoId}`); } catch (err) { @@ -282,6 +293,8 @@ export default function NewRepoPage() { setAutoResume={setAutoResume} autoMerge={autoMerge} setAutoMerge={setAutoMerge} + ticketIntegrationEnabled={ticketIntegrationEnabled} + setTicketIntegrationEnabled={setTicketIntegrationEnabled} selectClass={selectClass} inputClass={inputClass} /> @@ -626,6 +639,8 @@ function ReviewStep({ setAutoResume, autoMerge, setAutoMerge, + ticketIntegrationEnabled, + setTicketIntegrationEnabled, selectClass, inputClass, }: { @@ -643,6 +658,8 @@ function ReviewStep({ setAutoResume: (v: boolean) => void; autoMerge: boolean; setAutoMerge: (v: boolean) => void; + ticketIntegrationEnabled: boolean; + setTicketIntegrationEnabled: (v: boolean) => void; selectClass: string; inputClass: string; }) { @@ -738,6 +755,25 @@ function ReviewStep({ Auto-merge PR when checks pass and review completes + + {/* Ticket Integration */} +
+ +
); }