From 5e76374b9a755b5bbf907fed3b6a751d90ad2cd1 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 18 May 2026 17:32:07 -0700 Subject: [PATCH 1/5] chore(porch): bugfix-749 init bugfix --- .../status.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml diff --git a/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml b/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml new file mode 100644 index 00000000..59d26d11 --- /dev/null +++ b/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml @@ -0,0 +1,12 @@ +id: bugfix-749 +title: gitea-forge-500-error-bug-with +protocol: bugfix +phase: investigate +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:32:06.931Z' From 1c720fd0b6a052a8d7fe74f297563f0e7f5c59aa Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 18 May 2026 17:37:50 -0700 Subject: [PATCH 2/5] [Bugfix #749][Bugfix #750] Fix Gitea forge crash and field normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tightly-coupled Gitea-forge regressions reported in v3.0.3: #749 — parseLabelDefaults() crashed with `labels.map is not a function`, 500ing the Tower overview when Forgejo returned `labels: ""` (or null) for unlabeled issues. Coerce non-array inputs to [] so the array methods can't throw. Added regression tests exercising the empty-string, null, and undefined paths alongside the existing array (GitHub) path. #750 — Tower showed `#undefined` / `NaNd` because the Gitea preset scripts piped raw `tea` JSON straight to the overview, which expects the GitHub-compatible shape declared in forge-contracts.ts. Each script now requests an explicit `--fields` list and normalizes via jq: index (string) -> number (int) created -> createdAt author (string) -> author.login labels (CSV) -> labels[].name assignees (CSV) -> assignees[].login description -> body recently-closed maps `updated` -> `closedAt`; recently-merged filters by `.merged == true` (same predicate gitea/pr-exists.sh already uses) and maps `updated` -> `mergedAt`, `head.ref` -> `headRefName`. Fixes #749 Fixes #750 --- .../codev/scripts/forge/gitea/issue-list.sh | 25 +++++++++++++- packages/codev/scripts/forge/gitea/pr-list.sh | 22 +++++++++++- .../scripts/forge/gitea/recently-closed.sh | 20 ++++++++++- .../scripts/forge/gitea/recently-merged.sh | 26 +++++++++++++- packages/codev/src/__tests__/github.test.ts | 34 +++++++++++++++++++ packages/codev/src/lib/github.ts | 8 +++-- 6 files changed, 129 insertions(+), 6 deletions(-) diff --git a/packages/codev/scripts/forge/gitea/issue-list.sh b/packages/codev/scripts/forge/gitea/issue-list.sh index f1c56545..a8bc246f 100755 --- a/packages/codev/scripts/forge/gitea/issue-list.sh +++ b/packages/codev/scripts/forge/gitea/issue-list.sh @@ -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) + }]' diff --git a/packages/codev/scripts/forge/gitea/pr-list.sh b/packages/codev/scripts/forge/gitea/pr-list.sh index 70f3c190..90c52a9f 100755 --- a/packages/codev/scripts/forge/gitea/pr-list.sh +++ b/packages/codev/scripts/forge/gitea/pr-list.sh @@ -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} + }]' diff --git a/packages/codev/scripts/forge/gitea/recently-closed.sh b/packages/codev/scripts/forge/gitea/recently-closed.sh index f7da1bbf..1cd91e81 100755 --- a/packages/codev/scripts/forge/gitea/recently-closed.sh +++ b/packages/codev/scripts/forge/gitea/recently-closed.sh @@ -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) + }]' diff --git a/packages/codev/scripts/forge/gitea/recently-merged.sh b/packages/codev/scripts/forge/gitea/recently-merged.sh index 0f5815a6..4c2d3095 100755 --- a/packages/codev/scripts/forge/gitea/recently-merged.sh +++ b/packages/codev/scripts/forge/gitea/recently-merged.sh @@ -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 // "") + }]' diff --git a/packages/codev/src/__tests__/github.test.ts b/packages/codev/src/__tests__/github.test.ts index d186fa3a..f1c47e89 100644 --- a/packages/codev/src/__tests__/github.test.ts +++ b/packages/codev/src/__tests__/github.test.ts @@ -258,6 +258,40 @@ 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', () => { + // @ts-expect-error — exercising the non-GitHub forge runtime shape + expect(parseLabelDefaults('', 'Fix login bug')).toEqual({ + type: 'bug', + priority: 'medium', + }); + }); + + it('coerces null labels to no-labels result', () => { + // @ts-expect-error — exercising the non-GitHub forge runtime shape + expect(parseLabelDefaults(null)).toEqual({ + type: 'project', + priority: 'medium', + }); + }); + + it('coerces undefined labels to no-labels result', () => { + // @ts-expect-error — exercising the non-GitHub forge runtime shape + 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', + }); + }); }); // ============================================================================= diff --git a/packages/codev/src/lib/github.ts b/packages/codev/src/lib/github.ts index 2b539e30..5c8bc927 100644 --- a/packages/codev/src/lib/github.ts +++ b/packages/codev/src/lib/github.ts @@ -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:')) From a30add4c1672cee3b8f199f2653fed8cbad30abb Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 18 May 2026 17:38:00 -0700 Subject: [PATCH 3/5] chore(porch): bugfix-749 fix phase-transition --- .../bugfix-749-gitea-forge-500-error-bug-with/status.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml b/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml index 59d26d11..801ef03a 100644 --- a/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml +++ b/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml @@ -1,7 +1,7 @@ id: bugfix-749 title: gitea-forge-500-error-bug-with protocol: bugfix -phase: investigate +phase: fix plan_phases: [] current_plan_phase: null gates: {} @@ -9,4 +9,4 @@ iteration: 1 build_complete: false history: [] started_at: '2026-05-19T00:32:06.931Z' -updated_at: '2026-05-19T00:32:06.931Z' +updated_at: '2026-05-19T00:38:00.773Z' From 3a2bac26dda882017661f8ebac257406daf00c91 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 18 May 2026 17:38:33 -0700 Subject: [PATCH 4/5] chore(porch): bugfix-749 pr phase-transition --- .../bugfix-749-gitea-forge-500-error-bug-with/status.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml b/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml index 801ef03a..b258265d 100644 --- a/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml +++ b/codev/projects/bugfix-749-gitea-forge-500-error-bug-with/status.yaml @@ -1,7 +1,7 @@ id: bugfix-749 title: gitea-forge-500-error-bug-with protocol: bugfix -phase: fix +phase: pr plan_phases: [] current_plan_phase: null gates: {} @@ -9,4 +9,4 @@ iteration: 1 build_complete: false history: [] started_at: '2026-05-19T00:32:06.931Z' -updated_at: '2026-05-19T00:38:00.773Z' +updated_at: '2026-05-19T00:38:33.632Z' From 54ee8dffd44b17fce6f69c58bfa07e1c222bcf01 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Mon, 18 May 2026 21:00:19 -0700 Subject: [PATCH 5/5] [Bugfix #749] Remove stale @ts-expect-error directives in github.test.ts The labels parameter type in parseLabelDefaults() was widened to accept `string | null | undefined` as part of the #749 fix, so the @ts-expect-error directives in the new regression tests no longer have anything to suppress. Per Gemini CMAP cosmetic note. --- packages/codev/src/__tests__/github.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/codev/src/__tests__/github.test.ts b/packages/codev/src/__tests__/github.test.ts index f1c47e89..d4268498 100644 --- a/packages/codev/src/__tests__/github.test.ts +++ b/packages/codev/src/__tests__/github.test.ts @@ -263,7 +263,6 @@ describe('parseLabelDefaults', () => { // 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', () => { - // @ts-expect-error — exercising the non-GitHub forge runtime shape expect(parseLabelDefaults('', 'Fix login bug')).toEqual({ type: 'bug', priority: 'medium', @@ -271,7 +270,6 @@ describe('parseLabelDefaults', () => { }); it('coerces null labels to no-labels result', () => { - // @ts-expect-error — exercising the non-GitHub forge runtime shape expect(parseLabelDefaults(null)).toEqual({ type: 'project', priority: 'medium', @@ -279,7 +277,6 @@ describe('parseLabelDefaults', () => { }); it('coerces undefined labels to no-labels result', () => { - // @ts-expect-error — exercising the non-GitHub forge runtime shape expect(parseLabelDefaults(undefined, 'Add dark mode')).toEqual({ type: 'project', priority: 'medium',