Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
id: bugfix-749
title: gitea-forge-500-error-bug-with
protocol: bugfix
phase: pr
plan_phases: []
current_plan_phase: null
gates: {}
iteration: 1
build_complete: false
history: []
started_at: '2026-05-19T00:32:06.931Z'
updated_at: '2026-05-19T00:38:33.632Z'
25 changes: 24 additions & 1 deletion packages/codev/scripts/forge/gitea/issue-list.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
#!/bin/sh
# Forge concept: issue-list (Gitea via tea CLI)
exec tea issues list --limit 200 --output json
#
# tea's default JSON output uses fields that don't match the GitHub-compatible
# shape codev's overview expects (see codev/src/lib/forge-contracts.ts).
# Normalize via jq so the same overview code path works for both forges:
# index -> number (int)
# created -> createdAt
# author (string) -> author.login
# labels (CSV) -> labels[].name
# assignees (CSV) -> assignees[].login
exec tea issues list --limit 200 \
--fields index,title,state,author,url,created,labels,assignees \
--output json \
| jq '[.[] | {
number: (.index | tonumber),
title,
state,
url,
createdAt: .created,
author: {login: .author},
labels: (if (.labels // "") == "" then []
else (.labels | split(",") | map({name: ltrimstr(" ")})) end),
assignees: (if (.assignees // "") == "" then []
else (.assignees | split(",") | map({login: ltrimstr(" ")})) end)
}]'
22 changes: 21 additions & 1 deletion packages/codev/scripts/forge/gitea/pr-list.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
#!/bin/sh
# Forge concept: pr-list (Gitea via tea CLI)
exec tea pulls list --output json
#
# Normalize tea's PR shape to the GitHub-compatible shape codev expects
# (see PrListItem in codev/src/lib/forge-contracts.ts):
# index -> number (int)
# description -> body
# created -> createdAt
# author (string) -> author.login
# reviewDecision -> "" (Gitea has no GitHub-equivalent review-decision summary)
exec tea pulls list --limit 200 \
--fields index,title,state,author,url,created,description \
--output json \
| jq '[.[] | {
number: (.index | tonumber),
title,
state,
url,
reviewDecision: "",
body: (.description // ""),
createdAt: .created,
author: {login: .author}
}]'
20 changes: 19 additions & 1 deletion packages/codev/scripts/forge/gitea/recently-closed.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
#!/bin/sh
# Forge concept: recently-closed (Gitea via tea CLI)
exec tea issues list --state closed --limit 1000 --output json
#
# Normalize to GitHub-compatible shape (see IssueListItem in forge-contracts.ts).
# Gitea exposes no separate `closed_at` field on issue list output, so we map
# `updated` -> `closedAt`. For issues closed without subsequent edits this is
# exactly the close time; for issues edited after close it overestimates, which
# is acceptable for the "recently closed" overview filter.
exec tea issues list --state closed --limit 1000 \
--fields index,title,state,author,url,created,updated,labels \
--output json \
| jq '[.[] | {
number: (.index | tonumber),
title,
state,
url,
createdAt: .created,
closedAt: .updated,
labels: (if (.labels // "") == "" then []
else (.labels | split(",") | map({name: ltrimstr(" ")})) end)
}]'
26 changes: 25 additions & 1 deletion packages/codev/scripts/forge/gitea/recently-merged.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
#!/bin/sh
# Forge concept: recently-merged (Gitea via tea CLI)
exec tea pulls list --state closed --limit 1000 --output json
#
# `tea pulls list --state closed` returns both merged PRs and closed-without-
# merge PRs. Filter to merged only via `.merged == true` (the same predicate
# scripts/forge/gitea/pr-exists.sh already relies on), then map to the
# GitHub-compatible shape:
# index -> number (int)
# created -> createdAt
# updated -> mergedAt (tea exposes no merged_at field via --fields;
# close-then-edit overestimates merged time
# but is acceptable for the 24h overview window)
# head.ref -> headRefName
# description -> body
exec tea pulls list --state closed --limit 1000 \
--fields index,title,state,author,url,created,updated,head,description,merged \
--output json \
| jq '[.[] | select(.merged == true) | {
number: (.index | tonumber),
title,
state,
url,
body: (.description // ""),
createdAt: .created,
mergedAt: .updated,
headRefName: (.head.ref // "")
}]'
31 changes: 31 additions & 0 deletions packages/codev/src/__tests__/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,37 @@ describe('parseLabelDefaults', () => {
expect(parseLabelDefaults([], 'Create issue template').type).toBe('project');
expect(parseLabelDefaults([], 'Improve issue search').type).toBe('project');
});

// Regression: issue #749 — Gitea/Forgejo returns `labels: ""` or `null` for
// unlabeled issues, where GitHub always returns []. parseLabelDefaults used
// to crash with "labels.map is not a function" and 500 the Tower overview.
it('coerces empty-string labels (Gitea/Forgejo) to no-labels result', () => {
expect(parseLabelDefaults('', 'Fix login bug')).toEqual({
type: 'bug',
priority: 'medium',
});
});

it('coerces null labels to no-labels result', () => {
expect(parseLabelDefaults(null)).toEqual({
type: 'project',
priority: 'medium',
});
});

it('coerces undefined labels to no-labels result', () => {
expect(parseLabelDefaults(undefined, 'Add dark mode')).toEqual({
type: 'project',
priority: 'medium',
});
});

it('still extracts type from a real label array (GitHub path)', () => {
expect(parseLabelDefaults([{ name: 'type:bug' }])).toEqual({
type: 'bug',
priority: 'medium',
});
});
});

// =============================================================================
Expand Down
8 changes: 6 additions & 2 deletions packages/codev/src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,13 +466,17 @@ const BARE_TYPE_LABELS = new Set(['bug', 'project', 'spike']);
const BUG_TITLE_PATTERNS = /\b(fix|bug|broken|error|crash|fail|wrong|regression|not working)/i;

export function parseLabelDefaults(
labels: Array<{ name: string }>,
labels: Array<{ name: string }> | null | undefined | string,
title?: string,
): {
type: string;
priority: string;
} {
const names = labels.map(l => l.name);
// Forge providers vary: GitHub returns an array of {name} objects, while
// Gitea/Forgejo returns "" (empty string) or null when an issue has no
// labels. Coerce non-array inputs to [] so the array methods below can't
// throw "labels.map is not a function" in non-GitHub forges.
const names = Array.isArray(labels) ? labels.map(l => l.name) : [];

const typeLabels = names
.filter(n => n.startsWith('type:'))
Expand Down
Loading