Compare commits

..

15 Commits

Author SHA1 Message Date
dbd94e7b37 Merge pull request 'Make Claude Code footer stripping more robust' (#25) from probablycorey/fix-claude-footer-strip into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #25
2026-03-11 21:06:51 +00:00
c22c5c423c Make Claude Code footer stripping more robust
Some checks failed
CI / test (pull_request) Has been cancelled
Updated the regex to match the robot emoji followed by "Claude Code" on the last line, regardless of formatting. Also pulled the regex into a named variable with a comment to clarify intent. This catches variations like "Generated with Claude Code" without square brackets.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-11 13:18:24 -07:00
bb54e1048a Merge pull request 'Add Gitea API error logging' (#24) from probablycorey/spike-error-debug into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #24
2026-03-10 19:18:17 +00:00
829e1ea1dc Add detailed logging for Gitea API errors to diagnose 401 issues.
Some checks failed
CI / test (pull_request) Has been cancelled
Extracts token presence/length, response status, failed URL, and response body (first 500 chars) into structured logs. This will help identify whether token is missing, invalid, or if Gitea is rejecting for another reason.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 12:10:36 -07:00
f31b07caa7 Merge pull request 'Fix log file sorting to handle both timestamp and sha formats' (#23) from probablycorey/fix-log-display into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #23
2026-03-10 18:54:02 +00:00
30a6b0c987 Fix log file sorting to handle both timestamp and sha formats
Some checks failed
CI / test (pull_request) Has been cancelled
Replace naive sort with explicit timestamp extraction that works for both the new timestamp_sha format and legacy sha_timestamp format. Extract log filename parsing into a reusable function for consistency.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 11:53:38 -07:00
d850e00c4e Merge pull request 'Remove Claude Code footer from Discord PR messages' (#22) from probablycorey/remove-claude-footer into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #22
2026-03-10 18:44:51 +00:00
28cc3d4c94 Remove Claude Code footer from Discord PR messages
Some checks failed
CI / test (pull_request) Has been cancelled
Strip the "🤖 Generated with Claude Code" line from PR bodies before posting them to Discord. This line is added by Claude Code when creating PRs but is unnecessary in the Discord bridge.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 11:44:05 -07:00
a37dc8b503 Merge pull request 'Fix log file sorting in sidebar' (#20) from probablycorey/fix-log-sort into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #20
2026-03-10 18:38:41 +00:00
96aedc8239 Fix log file sorting by using timestamp-first filename format
Some checks failed
CI / test (pull_request) Has been cancelled
Filenames are now timestamp_sha.jsonl instead of sha_timestamp.jsonl. This makes alphabetical sort equal to chronological sort, eliminating fragile string parsing and ensuring commit SHAs are ordered by their most recent log file timestamp.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 11:35:28 -07:00
7408c02d64 Merge pull request 'Add raw JSONL endpoint and improve error display' (#17) from probablycorey/raw-jsonl-endpoint into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #17
2026-03-10 18:05:26 +00:00
2943f24c17 Merge pull request 'Document Gitea webhook setup and fix log file sorting' (#15) from probablycorey/pr-to-discord-flow into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #15
2026-03-10 18:02:49 +00:00
c3000e954b Merge branch 'main' into probablycorey/pr-to-discord-flow
Some checks failed
CI / test (pull_request) Has been cancelled
2026-03-10 18:02:44 +00:00
cb10121fb5 Merge pull request 'Add content negotiation for raw JSONL logs' (#16) from probablycorey/raw-jsonl-endpoint into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #16
2026-03-10 18:02:08 +00:00
bafb6fe93b Document Gitea webhook setup and fix log file sidebar sorting
Some checks failed
CI / test (pull_request) Has been cancelled
- Add setup instructions for system-wide and org/repo webhooks
- Fix sidebar to sort log file groups by most recent file timestamp
- Maintains chronological order when viewing logs across different git commits

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 10:38:29 -07:00
5 changed files with 71 additions and 31 deletions

View File

@ -10,7 +10,15 @@ Discord-Gitea bridge bot. When someone opens a PR or leaves a comment on [git.no
4. `bun run subdomain:dev` to start the server.
5. Visit `localhost:3000/discord/auth` to authorize the bot to your Discord server.
6. Use [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) or ngrok to expose your local server to the internet.
7. Add a Gitea webhook at `https://git.nose.space/<org>/settings/hooks` pointing to `https://<your-tunnel>/gitea/webhook`.
7. Add a Gitea webhook (see below).
### Gitea webhook
Spike needs a webhook so Gitea sends PR and comment events to it. You have two options:
**System webhook (covers all repos)** — Requires Gitea admin access. Go to **Site Administration > System Webhooks > Add Webhook > Gitea**. Set the target URL to `https://spike.theworkshop.cc/gitea/webhook`. Under "Trigger On", select **Custom Events** and enable Pull Request, Issue Comment, and Pull Request Comment. This fires for every repo on the instance.
**Org/repo webhook (covers one org or repo)** — Go to the org or repo settings, then **Webhooks > Add Webhook > Gitea**. Same target URL and event configuration as above. Only fires for that org or repo.
### Environment variables

View File

@ -112,7 +112,11 @@ class PRHandler {
}
static formatPrBody(pullRequest: Gitea.PullRequest, repositoryFullName: string): string {
const body = pullRequest.body || "_empty_"
// 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_"
let message = `
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
> **${repositoryFullName}**

View File

@ -1,18 +1,33 @@
import { log } from "../log"
import type { Gitea } from "./types"
const giteaUrl = "https://git.nose.space"
export async function fetchPR(fullname: string, prNumber: number): Promise<Gitea.PullRequest> {
const url = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}`
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> {
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>
}
@ -20,16 +35,8 @@ 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 fetch(reviewsUrl, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!reviewsResponse.ok) {
throw new Error(`Gitea API error: ${reviewsResponse.status} ${reviewsResponse.statusText}`)
}
const reviewsResponse = await giteaFetch(reviewsUrl)
const reviews = (await reviewsResponse.json()) as Array<{
id: number
@ -40,21 +47,12 @@ 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 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 commentsResponse = await giteaFetch(commentsUrl)
const comments = (await commentsResponse.json()) as Gitea.ReviewComment[]
allComments.push(...comments)
}

View File

@ -16,6 +16,7 @@ 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 }
@ -47,14 +48,20 @@ mkdirSync(logsDir, { recursive: true })
function createLogFile(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
return `${logsDir}/${releaseSha}_${timestamp}.jsonl`
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() || ""
}
export function listLogFiles(): string[] {
return readdirSync(logsDir)
.filter((f) => f.endsWith(".jsonl"))
.sort()
.reverse()
.sort((a, b) => extractTimestamp(b).localeCompare(extractTimestamp(a)))
.slice(0, 20)
}

View File

@ -108,27 +108,50 @@ 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
// Group files by sha, sorted by most recent file in each group
const grouped: Record<string, string[]> = {}
for (const file of files) {
const sha = file.split("_")[0] || "unknown"
const { sha } = parseLogFilename(file)
const list = grouped[sha] || (grouped[sha] = [])
list.push(file)
}
const sortedGroups = Object.entries(grouped).sort(([, a], [, b]) => {
return b[0]!.localeCompare(a[0]!)
})
return (
<nav style="width: 280px; min-width: 280px; background: #141414; border-right: 1px solid #2a2a2a; overflow-y: auto; padding: 16px 0">
<div style="padding: 0 16px 12px; color: #9ca3af; font-size: 11px; text-transform: uppercase; letter-spacing: 1px">
Log Files
</div>
{Object.entries(grouped).map(([sha, shaFiles]) => (
{sortedGroups.map(([sha, shaFiles]) => (
<div style="margin-bottom: 8px">
<div style="padding: 4px 16px; color: #6b7280; font-size: 11px; font-family: monospace">{sha}</div>
{shaFiles.map((file) => {
const isSelected = file === selectedFile
// 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 { isoStr } = parseLogFilename(file)
const bg = isSelected ? "#2a2a2a" : "transparent"
return (
<a