Portal issues now file with only the 'portal' label. The watcher runs two phases:
1. Triage — Claude reads each untriaged 'portal' issue (analysis only), posts a
requirements-breakdown comment, and routes it to 'claude-queue' (auto-fixable)
or 'interactive' (needs human steering).
2. Fix — unchanged; processes 'claude-queue' issues into PRs.
The triage breakdown is posted without the bot marker so the fix stage reads it
back as refined requirements.
PS 5.1 fixes found while validating:
- Send API bodies as UTF-8 bytes (Invoke-RestMethod mangled non-ASCII, e.g. the
em-dash in Claude's breakdown, so Forgejo rejected the JSON)
- Build the labels array body by hand (ConvertTo-Json unwraps a single-element
array to a scalar, which Forgejo rejects)
- Triage output via two plain files (label + markdown) instead of one JSON blob
(embedded-newline markdown broke ConvertFrom-Json)
- Read triage files as UTF-8; additive label POST + a guard so Set-IssueLabels
can never wipe an issue's labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
"use server";
|
|
|
|
import { auth } from "@/auth";
|
|
import { createForgejoIssue } from "@/lib/forgejo";
|
|
import { z } from "zod";
|
|
|
|
const PRIORITIES = ["P0 — Critical (broken / blocking)", "P1 — High", "P2 — Medium", "P3 — Low"] as const;
|
|
|
|
const reportSchema = z.object({
|
|
title: z.string().min(5, "Please give the issue a short title (min 5 characters)").max(150),
|
|
description: z.string().min(10, "Please describe the issue (min 10 characters)").max(5000),
|
|
priority: z.enum(PRIORITIES),
|
|
page: z.string().max(500).optional(),
|
|
});
|
|
|
|
type Result = { ok: true; issueNumber: number; issueUrl: string } | { error: string };
|
|
|
|
export async function reportIssue(formData: FormData): Promise<Result> {
|
|
const session = await auth();
|
|
if (!session?.user) return { error: "Unauthorized" };
|
|
|
|
const parsed = reportSchema.safeParse(Object.fromEntries(formData));
|
|
if (!parsed.success) return { error: parsed.error.errors[0].message };
|
|
const { title, description, priority, page } = parsed.data;
|
|
|
|
const body = [
|
|
"### Raised by",
|
|
`${session.user.name} (${session.user.email}, ${session.user.role}) — via portal Report Issue button`,
|
|
"",
|
|
"### Description",
|
|
description,
|
|
"",
|
|
"### Priority",
|
|
priority,
|
|
"",
|
|
"### Context",
|
|
`- Page: \`${page || "unknown"}\``,
|
|
`- Reported at: ${new Date().toISOString()}`,
|
|
].join("\n");
|
|
|
|
try {
|
|
// File with only `portal`. The watcher triages portal issues — Claude reads
|
|
// the issue, posts a requirements breakdown, and routes it to `claude-queue`
|
|
// (auto-fixable) or `interactive` (needs human steering).
|
|
const issue = await createForgejoIssue({
|
|
title: `[Issue]: ${title}`,
|
|
body,
|
|
labels: ["portal"],
|
|
});
|
|
return { ok: true, issueNumber: issue.number, issueUrl: issue.html_url };
|
|
} catch (err) {
|
|
console.error("reportIssue failed:", err);
|
|
return { error: "Could not file the issue. Please try again or contact the administrator." };
|
|
}
|
|
}
|