Compare commits

..

No commits in common. "main" and "probablycorey/pr-to-discord-flow" have entirely different histories.

4 changed files with 34 additions and 86 deletions

View File

@ -112,11 +112,7 @@ class PRHandler {
}
static formatPrBody(pullRequest: Gitea.PullRequest, repositoryFullName: string): string {
// Strip the "🤖 Generated with Claude Code" footer that gets appended to PR bodies
const claudeFooter = /\n*🤖.*Claude Code[^\n]*$/
const body = (pullRequest.body || "_empty_")
.replace(claudeFooter, "")
.trim() || "_empty_"
const body = pullRequest.body || "_empty_"
let message = `
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
> **${repositoryFullName}**

View File

@ -1,33 +1,18 @@
import { log } from "../log"
import type { Gitea } from "./types"
const giteaUrl = "https://git.nose.space"
function tokenPrefix(): string {
const token = process.env.GITEA_API_TOKEN
if (!token) return "MISSING"
return `${token.slice(0, 4)}...${token.slice(-4)} (${token.length} chars)`
}
async function giteaFetch(url: string): Promise<Response> {
export async function fetchPR(fullname: string, prNumber: number): Promise<Gitea.PullRequest> {
const url = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}`
const response = await fetch(url, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!response.ok) {
const body = await response.text()
log({ type: "gitea-api-error", status: response.status, url, body: body.slice(0, 500), tokenPrefix: tokenPrefix() })
throw new Error(`Gitea API error: ${response.status} ${response.statusText}`)
}
return response
}
export async function fetchPR(fullname: string, prNumber: number): Promise<Gitea.PullRequest> {
const url = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}`
const response = await giteaFetch(url)
return response.json() as Promise<Gitea.PullRequest>
}
@ -35,8 +20,16 @@ export async function fetchReviewComments(
fullname: string,
prNumber: number
): Promise<Gitea.ReviewComment[]> {
// First, fetch all reviews
const reviewsUrl = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}/reviews`
const reviewsResponse = await giteaFetch(reviewsUrl)
const reviewsResponse = await fetch(reviewsUrl, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!reviewsResponse.ok) {
throw new Error(`Gitea API error: ${reviewsResponse.status} ${reviewsResponse.statusText}`)
}
const reviews = (await reviewsResponse.json()) as Array<{
id: number
@ -47,12 +40,21 @@ export async function fetchReviewComments(
comments_count: number
}>
// For each review, fetch its comments
const allComments: Gitea.ReviewComment[] = []
for (const review of reviews) {
if (review.comments_count === 0) continue
const commentsUrl = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}/reviews/${review.id}/comments`
const commentsResponse = await giteaFetch(commentsUrl)
const commentsResponse = await fetch(commentsUrl, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!commentsResponse.ok) {
throw new Error(`Gitea API error fetching review ${review.id} comments: ${commentsResponse.status} ${commentsResponse.statusText}`)
}
const comments = (await commentsResponse.json()) as Gitea.ReviewComment[]
allComments.push(...comments)
}

View File

@ -16,7 +16,6 @@ export type LogEvent =
| { type: "crash-log-found" }
| { type: "startup"; detail: string }
| { type: "discord-ready" }
| { type: "gitea-api-error"; status: number; url: string; body: string; tokenPrefix: string }
| { type: "error"; error: unknown; context?: string }
export type StoredLogEvent = LogEvent & { ts: string }
@ -48,20 +47,14 @@ mkdirSync(logsDir, { recursive: true })
function createLogFile(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
return `${logsDir}/${timestamp}_${releaseSha}.jsonl`
}
function extractTimestamp(filename: string): string {
const stem = filename.replace(".jsonl", "")
// New format: timestamp_sha, Old format: sha_timestamp
if (/^\d{4}-/.test(stem)) return stem.split("_").slice(0, -1).join("_")
return stem.split("_").pop() || ""
return `${logsDir}/${releaseSha}_${timestamp}.jsonl`
}
export function listLogFiles(): string[] {
return readdirSync(logsDir)
.filter((f) => f.endsWith(".jsonl"))
.sort((a, b) => extractTimestamp(b).localeCompare(extractTimestamp(a)))
.sort()
.reverse()
.slice(0, 20)
}

View File

@ -19,28 +19,11 @@ const typeColors: Record<string, string> = {
const summaryFields = ["type", "ts"] as const
function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === "object" && error && "message" in error) return String((error as any).message)
return String(error)
}
function errorStack(error: unknown): string | undefined {
if (error instanceof Error) return error.stack
if (typeof error === "object" && error && "stack" in error) return String((error as any).stack)
}
function getEventMeta(event: StoredLogEvent): Record<string, string> {
const meta: Record<string, string> = {}
for (const [key, value] of Object.entries(event)) {
if (summaryFields.includes(key as any)) continue
if (value === undefined) continue
if (key === "error") {
meta[key] = errorMessage(value)
const stack = errorStack(value)
if (stack) meta.stack = stack
continue
}
meta[key] = typeof value === "string" ? value : String(value)
}
return meta
@ -60,7 +43,8 @@ function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
if ("eventType" in event) details.push(event.eventType)
if (event.type === "error" && event.context) details.push(event.context)
if (event.type === "error") {
details.push(errorMessage(event.error))
const msg = event.error instanceof Error ? event.error.message : String(event.error)
details.push(msg)
}
const meta = getEventMeta(event)
@ -88,17 +72,10 @@ function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
<td colspan="3" style="padding: 8px 12px 12px">
<div style="display: flex; flex-wrap: wrap; gap: 6px 20px; font-size: 12px">
{Object.entries(meta).map(([key, value]) => (
key === "stack" ? (
<div style="width: 100%">
<span style="color: #6b7280">{key}</span>
<pre style="color: #d1d5db; margin: 4px 0 0; font-size: 11px; white-space: pre-wrap; opacity: 0.8">{value}</pre>
</div>
) : (
<div style="display: flex; gap: 6px">
<span style="color: #6b7280">{key}</span>
<span style="color: #d1d5db; max-width: 500px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">{value}</span>
</div>
)
<div style="display: flex; gap: 6px">
<span style="color: #6b7280">{key}</span>
<span style="color: #d1d5db; max-width: 500px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">{value}</span>
</div>
))}
</div>
</td>
@ -108,32 +85,11 @@ function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
)
}
function parseLogFilename(file: string): { sha: string; isoStr: string } {
const stem = file.replace(".jsonl", "")
// New format: timestamp_sha (e.g. 2026-03-10T18-06-23-561Z_cb10121)
// Old format: sha_timestamp (e.g. cb10121_2026-03-10T18-06-23-561Z)
const isNewFormat = /^\d{4}-/.test(stem)
const parts = stem.split("_")
let sha: string
let rawTimestamp: string
if (isNewFormat) {
sha = parts.pop() || "unknown"
rawTimestamp = parts.join("_")
} else {
rawTimestamp = parts.pop() || ""
sha = parts.join("_") || "unknown"
}
const isoStr = rawTimestamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d+)Z/, "T$1:$2:$3.$4Z")
return { sha, isoStr }
}
function Sidebar({ files, selectedFile }: { files: string[]; selectedFile?: string }) {
// Group files by sha, sorted by most recent file in each group
const grouped: Record<string, string[]> = {}
for (const file of files) {
const { sha } = parseLogFilename(file)
const sha = file.split("_")[0] || "unknown"
const list = grouped[sha] || (grouped[sha] = [])
list.push(file)
}
@ -151,7 +107,8 @@ function Sidebar({ files, selectedFile }: { files: string[]; selectedFile?: stri
<div style="padding: 4px 16px; color: #6b7280; font-size: 11px; font-family: monospace">{sha}</div>
{shaFiles.map((file) => {
const isSelected = file === selectedFile
const { isoStr } = parseLogFilename(file)
// Parse ISO timestamp back from filename: sha_2026-03-10T02-56-59-938Z.jsonl
const isoStr = file.replace(/^[^_]+_/, "").replace(".jsonl", "").replace(/T(\d{2})-(\d{2})-(\d{2})-(\d+)Z/, "T$1:$2:$3.$4Z")
const bg = isSelected ? "#2a2a2a" : "transparent"
return (
<a