Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/api/src/routes/github-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
Expand Down
14 changes: 13 additions & 1 deletion apps/api/src/routes/request-input-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
27 changes: 26 additions & 1 deletion apps/api/src/routes/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────

Expand Down Expand Up @@ -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) => {
Expand All @@ -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({
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/app/repos/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -282,6 +293,8 @@ export default function NewRepoPage() {
setAutoResume={setAutoResume}
autoMerge={autoMerge}
setAutoMerge={setAutoMerge}
ticketIntegrationEnabled={ticketIntegrationEnabled}
setTicketIntegrationEnabled={setTicketIntegrationEnabled}
selectClass={selectClass}
inputClass={inputClass}
/>
Expand Down Expand Up @@ -626,6 +639,8 @@ function ReviewStep({
setAutoResume,
autoMerge,
setAutoMerge,
ticketIntegrationEnabled,
setTicketIntegrationEnabled,
selectClass,
inputClass,
}: {
Expand All @@ -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;
}) {
Expand Down Expand Up @@ -738,6 +755,25 @@ function ReviewStep({
<span className="text-sm">Auto-merge PR when checks pass and review completes</span>
</label>
</div>

{/* Ticket Integration */}
<div className="pt-2 border-t border-border/50">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={ticketIntegrationEnabled}
onChange={(e) => setTicketIntegrationEnabled(e.target.checked)}
className="w-4 h-4 rounded"
/>
<div>
<span className="text-sm">Enable GitHub Issues integration</span>
<p className="text-[10px] text-text-muted mt-0.5">
Sync issues labeled with <code className="px-1 bg-bg rounded">optio</code> for this
repository.
</p>
</div>
</label>
</div>
</section>
);
}
Loading