diff --git a/docs/forgejo.md b/docs/forgejo.md index f02dc1b..781e662 100644 --- a/docs/forgejo.md +++ b/docs/forgejo.md @@ -88,6 +88,8 @@ The action detects the platform using a three-tier strategy: **Recommendation**: Always set `platform: "forgejo"` explicitly. Auto-detection works but the explicit input eliminates ambiguity. +> **Note for GitHub Enterprise Server (GHES) users**: The tier-3 heuristic classifies any non-`github.com` URL as Forgejo. If you run on GHES (e.g., `https://github.mycompany.com`), you **must** set `platform: "github"` explicitly to avoid misdetection. + ## Differences from GitHub ### Authentication @@ -340,6 +342,7 @@ All execution modes and trigger types have been validated end-to-end on a Forgej ## Known Limitations +- **GitHub Enterprise Server (GHES) misdetection**: The tier-3 platform detection heuristic treats any `GITHUB_SERVER_URL` that isn't `github.com` as Forgejo. GHES instances (e.g., `https://github.mycompany.com`) will be misidentified. Set `platform: "github"` explicitly when running on GHES. - **Git ref API**: Forgejo uses `/git/refs` (plural) instead of GitHub's `/git/ref` (singular). The action handles this by using `repos.getBranch()` on Forgejo for branch SHA lookups. - **User display name fetch**: `GET /users/{username}` returns 403 without `read:user` scope. Non-fatal — falls back to login name. - **`pull_request_target` event**: May not exist in Forgejo. The action degrades gracefully (maps to `pull_request`). diff --git a/prds/7-senior-review-forgejo.md b/prds/7-senior-review-forgejo.md index 4d7279d..b6ba915 100644 --- a/prds/7-senior-review-forgejo.md +++ b/prds/7-senior-review-forgejo.md @@ -58,7 +58,7 @@ Perform a thorough, documented code review of all Forgejo-related changes. The r ## Milestones -- [ ] **M1: Architecture & design review** +- [x] **M1: Architecture & design review** - Review the lean adapter pattern: is it the right abstraction level? - Assess merge-conflict surface with upstream: are platform branches minimal? diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index de08d2c..89158f9 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -8,7 +8,6 @@ import { type CommentUpdateInput, } from "../github/operations/comment-logic"; import { - updateForgejoCommentBody, createForgejoPrCompareUrl, getForgejoRunIndex, } from "../forgejo/comment-logic"; @@ -19,7 +18,7 @@ import { isEntityContext, } from "../github/context"; import type { ParsedGitHubContext } from "../github/context"; -import { GITHUB_SERVER_URL } from "../github/api/config"; +import { getServerUrl } from "../github/api/config"; import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; @@ -54,7 +53,7 @@ export async function updateCommentLink( const { owner, repo } = context.repository; const platform = detectPlatform(); - const serverUrl = GITHUB_SERVER_URL; + const serverUrl = getServerUrl(); const runIdentifier = platform === "forgejo" ? getForgejoRunIndex() : process.env.GITHUB_RUN_ID; const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${runIdentifier}`; @@ -250,10 +249,7 @@ export async function updateCommentLink( errorDetails, }; - const updatedBody = - platform === "forgejo" - ? updateForgejoCommentBody(commentInput) - : updateCommentBody(commentInput); + const updatedBody = updateCommentBody(commentInput); try { await updateClaudeComment(octokit.rest, { diff --git a/src/forgejo/comment-logic.ts b/src/forgejo/comment-logic.ts index 2fccefb..7c1b458 100644 --- a/src/forgejo/comment-logic.ts +++ b/src/forgejo/comment-logic.ts @@ -1,18 +1,16 @@ /** * Forgejo-specific comment URL formatting and body construction. * - * Mirrors the GitHub versions in: - * - src/github/operations/comments/common.ts (spinner, link builders, initial comment) - * - src/github/operations/comment-logic.ts (updateCommentBody for final comment) - * - * Differences from GitHub: + * Contains Forgejo-specific helpers for: * - Unicode spinner instead of GitHub CDN image (CDN URLs don't resolve on Forgejo) - * - PR compare URL omits `quick_pull=1` (GitHub-specific parameter) - * - Dynamic server URL extraction instead of hardcoded github.com regex + * - PR compare URL with `expand=1` instead of GitHub's `quick_pull=1` + * - Forgejo run index (GITHUB_RUN_NUMBER vs GITHUB_RUN_ID) + * + * The final comment body logic (updateCommentBody) is unified in + * src/github/operations/comment-logic.ts and works for both platforms. */ -import { ensureProperlyEncodedUrl } from "../github/operations/comment-logic"; -import type { CommentUpdateInput } from "../github/operations/comment-logic"; +import { getServerUrl } from "../github/api/config"; // Re-export types so consumers don't need to import from two places export type { @@ -21,19 +19,6 @@ export type { } from "../github/operations/comment-logic"; export { ensureProperlyEncodedUrl } from "../github/operations/comment-logic"; -/** - * Read the server URL at call time (not module load time). - * - * The GitHub module uses a top-level constant from config.ts, which is fine - * when GITHUB_SERVER_URL doesn't change. For Forgejo we read from env at - * call time so that: - * 1. Tests can override the env var and see the effect immediately. - * 2. The same module works regardless of import order. - */ -function getServerUrl(): string { - return process.env.GITHUB_SERVER_URL || "https://github.com"; -} - // --------------------------------------------------------------------------- // Run index helper // --------------------------------------------------------------------------- @@ -118,189 +103,3 @@ I'll analyze this and get back to you. ${jobRunLink}${branchLink}`; } - -// --------------------------------------------------------------------------- -// Owner/repo extraction helper -// --------------------------------------------------------------------------- - -/** - * Extract owner/repo from a URL using the configured server URL. - * - * Unlike the GitHub version (which hardcodes `github.com`), this dynamically - * matches against any server URL — works for both GitHub and Forgejo. - */ -export function extractOwnerRepoFromUrl( - url: string, - serverUrl: string = getServerUrl(), -): { owner: string; repo: string } | null { - try { - const parsedServer = new URL(serverUrl); - const parsedUrl = new URL(url); - - // Host must match (handles both github.com and git.wxs.ro etc.) - if (parsedUrl.host !== parsedServer.host) { - return null; - } - - // Extract first two path segments as owner/repo - const segments = parsedUrl.pathname.split("/").filter(Boolean); - const owner = segments[0]; - const repo = segments[1]; - if (owner && repo) { - return { owner, repo }; - } - } catch { - // Invalid URLs — fall through to null - } - return null; -} - -// --------------------------------------------------------------------------- -// Final comment body (shown after Claude finishes) -// --------------------------------------------------------------------------- - -/** - * Forgejo version of updateCommentBody. - * - * Identical logic to src/github/operations/comment-logic.ts:updateCommentBody - * with one critical fix: uses extractOwnerRepoFromUrl() for dynamic server URL - * extraction instead of the hardcoded `github.com` regex. - */ -export function updateForgejoCommentBody(input: CommentUpdateInput): string { - const originalBody = input.currentBody; - const { - executionDetails, - jobUrl, - branchLink, - prLink, - actionFailed, - branchName, - triggerUsername, - errorDetails, - } = input; - - // Extract content from the original comment body - // First, remove the "Claude Code is working…" or "Claude Code is working..." message - const workingPattern = /Claude Code is working[…\.]{1,3}(?:\s*]*>)?/i; - let bodyContent = originalBody.replace(workingPattern, "").trim(); - - // Also remove the Unicode spinner used on Forgejo - bodyContent = bodyContent.replace(/⏳/g, "").trim(); - - // Check if there's a PR link in the content - let prLinkFromContent = ""; - - // Match the entire markdown link structure - const prLinkPattern = /\[Create .* PR\]\((.*)\)$/m; - const prLinkMatch = bodyContent.match(prLinkPattern); - - if (prLinkMatch && prLinkMatch[1]) { - const encodedUrl = ensureProperlyEncodedUrl(prLinkMatch[1]); - if (encodedUrl) { - prLinkFromContent = encodedUrl; - // Remove the PR link from the content - bodyContent = bodyContent.replace(prLinkMatch[0], "").trim(); - } - } - - // Calculate duration string if available - let durationStr = ""; - if (executionDetails?.duration_ms !== undefined) { - const totalSeconds = Math.round(executionDetails.duration_ms / 1000); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - durationStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; - } - - // Build the header - let header = ""; - - if (actionFailed) { - header = "**Claude encountered an error"; - if (durationStr) { - header += ` after ${durationStr}`; - } - header += "**"; - } else { - // Get the username from triggerUsername or extract from content - const usernameMatch = bodyContent.match(/@([a-zA-Z0-9-]+)/); - const username = - triggerUsername || (usernameMatch ? usernameMatch[1] : "user"); - - header = `**Claude finished @${username}'s task`; - if (durationStr) { - header += ` in ${durationStr}`; - } - header += "**"; - } - - // Add links section - let links = ` —— [View job](${jobUrl})`; - - // Add branch name with link - if (branchName || branchLink) { - let finalBranchName = branchName; - let branchUrl = ""; - - if (branchLink) { - // Extract the branch URL from the link - const urlMatch = branchLink.match(/\((https?:\/\/.*)\)/); - if (urlMatch && urlMatch[1]) { - branchUrl = urlMatch[1]; - } - - // Extract branch name from link if not provided - if (!finalBranchName) { - const branchNameMatch = branchLink.match(/tree\/([^"'\)]+)/); - if (branchNameMatch) { - finalBranchName = branchNameMatch[1]; - } - } - } - - // If we don't have a URL yet but have a branch name, construct it - // FIX: Uses extractOwnerRepoFromUrl instead of hardcoded github.com regex - if (!branchUrl && finalBranchName) { - const extracted = extractOwnerRepoFromUrl(jobUrl); - if (extracted) { - branchUrl = `${getServerUrl()}/${extracted.owner}/${extracted.repo}/tree/${finalBranchName}`; - } - } - - if (finalBranchName && branchUrl) { - links += ` • [\`${finalBranchName}\`](${branchUrl})`; - } else if (finalBranchName) { - links += ` • \`${finalBranchName}\``; - } - } - - // Add PR link (either from content or provided) - const prUrl = - prLinkFromContent || (prLink ? prLink.match(/\(([^)]+)\)/)?.[1] : ""); - if (prUrl) { - links += ` • [Create PR ➔](${prUrl})`; - } - - // Build the new body with blank line between header and separator - let newBody = `${header}${links}`; - - // Add error details if available - if (actionFailed && errorDetails) { - newBody += `\n\n\`\`\`\n${errorDetails}\n\`\`\``; - } - - newBody += `\n\n---\n`; - - // Clean up the body content - // Remove any existing View job run, branch links from the bottom - bodyContent = bodyContent.replace(/\n?\[View job run\]\([^\)]+\)/g, ""); - bodyContent = bodyContent.replace(/\n?\[View branch\]\([^\)]+\)/g, ""); - - // Remove any existing duration info at the bottom - bodyContent = bodyContent.replace(/\n*---\n*Duration: [0-9]+m? [0-9]+s/g, ""); - - // Add the cleaned body content - newBody += bodyContent; - - return newBody.trim(); -} diff --git a/src/github/api/config.ts b/src/github/api/config.ts index 9e533e5..026a859 100644 --- a/src/github/api/config.ts +++ b/src/github/api/config.ts @@ -2,3 +2,13 @@ export const GITHUB_API_URL = process.env.GITHUB_API_URL || "https://api.github.com"; export const GITHUB_SERVER_URL = process.env.GITHUB_SERVER_URL || "https://github.com"; + +/** + * Read the server URL at call time (not module load time). + * + * Use this instead of GITHUB_SERVER_URL in code paths where tests may + * override process.env.GITHUB_SERVER_URL after module import. + */ +export function getServerUrl(): string { + return process.env.GITHUB_SERVER_URL || "https://github.com"; +} diff --git a/src/github/operations/comment-logic.ts b/src/github/operations/comment-logic.ts index 03b5d86..c490df5 100644 --- a/src/github/operations/comment-logic.ts +++ b/src/github/operations/comment-logic.ts @@ -1,4 +1,4 @@ -import { GITHUB_SERVER_URL } from "../api/config"; +import { getServerUrl } from "../api/config"; export type ExecutionDetails = { total_cost_usd?: number; @@ -66,6 +66,37 @@ export function ensureProperlyEncodedUrl(url: string): string | null { } } +/** + * Extract owner/repo from a URL using the configured server URL. + * + * Dynamically matches against any server URL — works for both GitHub and Forgejo. + */ +export function extractOwnerRepoFromUrl( + url: string, + serverUrl: string = getServerUrl(), +): { owner: string; repo: string } | null { + try { + const parsedServer = new URL(serverUrl); + const parsedUrl = new URL(url); + + // Host must match (handles both github.com and custom domains) + if (parsedUrl.host !== parsedServer.host) { + return null; + } + + // Extract first two path segments as owner/repo + const segments = parsedUrl.pathname.split("/").filter(Boolean); + const owner = segments[0]; + const repo = segments[1]; + if (owner && repo) { + return { owner, repo }; + } + } catch { + // Invalid URLs — fall through to null + } + return null; +} + export function updateCommentBody(input: CommentUpdateInput): string { const originalBody = input.currentBody; const { @@ -84,6 +115,9 @@ export function updateCommentBody(input: CommentUpdateInput): string { const workingPattern = /Claude Code is working[…\.]{1,3}(?:\s*]*>)?/i; let bodyContent = originalBody.replace(workingPattern, "").trim(); + // Also remove the Unicode spinner used on Forgejo (safe no-op on GitHub) + bodyContent = bodyContent.replace(/⏳/g, "").trim(); + // Check if there's a PR link in the content let prLinkFromContent = ""; @@ -141,7 +175,7 @@ export function updateCommentBody(input: CommentUpdateInput): string { if (branchLink) { // Extract the branch URL from the link - const urlMatch = branchLink.match(/\((https:\/\/.*)\)/); + const urlMatch = branchLink.match(/\((https?:\/\/.*)\)/); if (urlMatch && urlMatch[1]) { branchUrl = urlMatch[1]; } @@ -156,11 +190,11 @@ export function updateCommentBody(input: CommentUpdateInput): string { } // If we don't have a URL yet but have a branch name, construct it + // Uses dynamic URL extraction that works for both GitHub and Forgejo if (!branchUrl && finalBranchName) { - // Extract owner/repo from jobUrl - const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//); - if (repoMatch) { - branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`; + const extracted = extractOwnerRepoFromUrl(jobUrl); + if (extracted) { + branchUrl = `${getServerUrl()}/${extracted.owner}/${extracted.repo}/tree/${finalBranchName}`; } } diff --git a/src/github/operations/comments/create-initial.ts b/src/github/operations/comments/create-initial.ts index e9d044b..d53e5da 100644 --- a/src/github/operations/comments/create-initial.ts +++ b/src/github/operations/comments/create-initial.ts @@ -56,9 +56,14 @@ export async function createInitialComment( const botNameMatch = comment.user?.type === "Bot" && comment.user?.login.toLowerCase().includes("claude"); + // On Forgejo, the bot is a regular user account identified by bot_name input + const forgejoBotMatch = + platform === "forgejo" && + context.inputs.botName && + comment.user?.login === context.inputs.botName; const bodyMatch = comment.body === initialBody; - return idMatch || botNameMatch || bodyMatch; + return idMatch || botNameMatch || forgejoBotMatch || bodyMatch; }); if (existingComment) { response = await octokit.rest.issues.updateComment({ diff --git a/src/github/operations/comments/update-with-branch.ts b/src/github/operations/comments/update-with-branch.ts index 6059f72..62eadcd 100644 --- a/src/github/operations/comments/update-with-branch.ts +++ b/src/github/operations/comments/update-with-branch.ts @@ -14,6 +14,7 @@ import { createForgejoJobRunLink, createForgejoBranchLink, createForgejoCommentBody, + getForgejoRunIndex, } from "../../../forgejo/comment-logic"; import { detectPlatform } from "../../../platform/detector"; import { type Octokits } from "../../api/client"; @@ -34,7 +35,7 @@ export async function updateTrackingComment( const jobRunLink = platform === "forgejo" - ? createForgejoJobRunLink(owner, repo, context.runId) + ? createForgejoJobRunLink(owner, repo, getForgejoRunIndex()) : createJobRunLink(owner, repo, context.runId); // Add branch link for issues (not PRs) diff --git a/src/platform/detector.ts b/src/platform/detector.ts index a40a30f..9938939 100644 --- a/src/platform/detector.ts +++ b/src/platform/detector.ts @@ -6,24 +6,47 @@ * 2. FORGEJO env var (set by Forgejo runner v7.0+) * 3. GITHUB_SERVER_URL heuristic (non-github.com = Forgejo) * 4. Default to GitHub + * + * The result is memoized after first detection — platform doesn't change + * at runtime and this avoids redundant env reads across 12+ call sites. */ export type Platform = "github" | "forgejo"; +let _cachedPlatform: Platform | undefined; + export function detectPlatform(): Platform { + if (_cachedPlatform !== undefined) return _cachedPlatform; + + let result: Platform = "github"; + // 1. Explicit platform from action.yml detect step or user input const platformEnv = process.env.PLATFORM; - if (platformEnv === "forgejo") return "forgejo"; - if (platformEnv === "github") return "github"; - + if (platformEnv === "forgejo") { + result = "forgejo"; + } else if (platformEnv === "github") { + result = "github"; + } // 2. FORGEJO env var (set by Forgejo runner v7.0+) - if (process.env.FORGEJO === "true") return "forgejo"; - + else if (process.env.FORGEJO === "true") { + result = "forgejo"; + } // 3. GITHUB_SERVER_URL heuristic — if not github.com, likely Forgejo/Gitea - const serverUrl = process.env.GITHUB_SERVER_URL || ""; - if (serverUrl && !serverUrl.includes("github.com")) { - return "forgejo"; + else { + const serverUrl = process.env.GITHUB_SERVER_URL || ""; + if (serverUrl && !serverUrl.includes("github.com")) { + result = "forgejo"; + } } - return "github"; + _cachedPlatform = result; + return result; +} + +/** + * Reset the memoized platform detection result. + * Only needed in tests that change platform-related env vars between cases. + */ +export function _resetPlatformCache(): void { + _cachedPlatform = undefined; } diff --git a/test/actor.test.ts b/test/actor.test.ts index 946347a..1c216ad 100644 --- a/test/actor.test.ts +++ b/test/actor.test.ts @@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"; import { checkHumanActor } from "../src/github/validation/actor"; +import { _resetPlatformCache } from "../src/platform/detector"; import type { Octokit } from "@octokit/rest"; import { createMockContext } from "./mockContext"; @@ -116,6 +117,7 @@ describe("checkHumanActor", () => { let savedServerUrl: string | undefined; beforeEach(() => { + _resetPlatformCache(); savedPlatform = process.env.PLATFORM; savedServerUrl = process.env.GITHUB_SERVER_URL; process.env.PLATFORM = "forgejo"; @@ -127,6 +129,7 @@ describe("checkHumanActor", () => { if (savedServerUrl !== undefined) process.env.GITHUB_SERVER_URL = savedServerUrl; else delete process.env.GITHUB_SERVER_URL; + _resetPlatformCache(); }); test("should skip actor check entirely on Forgejo", async () => { diff --git a/test/forgejo-comment-logic.test.ts b/test/forgejo-comment-logic.test.ts index 093bfe8..8466b5f 100644 --- a/test/forgejo-comment-logic.test.ts +++ b/test/forgejo-comment-logic.test.ts @@ -6,10 +6,12 @@ import { createForgejoBranchLink, createForgejoPrCompareUrl, createForgejoCommentBody, - extractOwnerRepoFromUrl, - updateForgejoCommentBody, - type CommentUpdateInput, } from "../src/forgejo/comment-logic"; +import { + extractOwnerRepoFromUrl, + updateCommentBody, + type CommentUpdateInput, +} from "../src/github/operations/comment-logic"; // --------------------------------------------------------------------------- // Test helpers @@ -307,10 +309,10 @@ describe("extractOwnerRepoFromUrl", () => { }); // --------------------------------------------------------------------------- -// updateForgejoCommentBody +// updateCommentBody // --------------------------------------------------------------------------- -describe("updateForgejoCommentBody", () => { +describe("updateCommentBody", () => { const forgejoBaseInput: CommentUpdateInput = { currentBody: "Initial comment body", actionFailed: false, @@ -332,7 +334,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "testuser", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task**"); expect(result).not.toContain("Claude Code is working"); expect(result).not.toContain(" { triggerUsername: "testuser", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task**"); expect(result).not.toContain("Claude Code is working"); expect(result).not.toContain("⏳"); @@ -359,7 +361,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "trigger-user", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain( "**Claude finished @trigger-user's task in 1m 14s**", ); @@ -373,7 +375,7 @@ describe("updateForgejoCommentBody", () => { executionDetails: { duration_ms: 45000 }, }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("**Claude encountered an error after 45s**"); }); @@ -386,7 +388,7 @@ describe("updateForgejoCommentBody", () => { errorDetails: "Failed to fetch issue data", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("**Claude encountered an error after 45s**"); expect(result).toContain("[View job]"); expect(result).toContain("```\nFailed to fetch issue data\n```"); @@ -400,7 +402,7 @@ describe("updateForgejoCommentBody", () => { currentBody: "Some comment", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain(`—— [View job](${forgejoBaseInput.jobUrl})`); }); }); @@ -412,7 +414,7 @@ describe("updateForgejoCommentBody", () => { branchName: "claude/issue-5-20240101-1200", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); // This is the critical fix: GitHub's version uses a hardcoded github.com regex // which fails for Forgejo URLs. Our version uses extractOwnerRepoFromUrl. expect(result).toContain( @@ -427,7 +429,7 @@ describe("updateForgejoCommentBody", () => { branchName: "feature-branch", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("• `feature-branch`"); expect(result).not.toContain("[`feature-branch`]"); }); @@ -440,7 +442,7 @@ describe("updateForgejoCommentBody", () => { "\n[View branch](https://git.wxs.ro/wxs/myrepo/tree/feature-branch)", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain( "• [`feature-branch`](https://git.wxs.ro/wxs/myrepo/tree/feature-branch)", ); @@ -453,7 +455,7 @@ describe("updateForgejoCommentBody", () => { "\n[View branch](https://git.wxs.ro/wxs/myrepo/tree/auto-detected-branch)", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain( "• [`auto-detected-branch`](https://git.wxs.ro/wxs/myrepo/tree/auto-detected-branch)", ); @@ -468,7 +470,7 @@ describe("updateForgejoCommentBody", () => { "\n[Create a PR](https://git.wxs.ro/wxs/myrepo/compare/main...feature)", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain( "• [Create PR ➔](https://git.wxs.ro/wxs/myrepo/compare/main...feature)", ); @@ -481,7 +483,7 @@ describe("updateForgejoCommentBody", () => { "Some comment with [Create a PR](https://git.wxs.ro/wxs/myrepo/compare/main...fix)", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain( "• [Create PR ➔](https://git.wxs.ro/wxs/myrepo/compare/main...fix)", ); @@ -496,7 +498,7 @@ describe("updateForgejoCommentBody", () => { currentBody: `Done!\n\n[Create a PR](${forgejoUrl})`, }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain(`• [Create PR ➔](${forgejoUrl})`); }); }); @@ -509,7 +511,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "testuser", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain( "**Claude finished @testuser's task in 1m 15s**", ); @@ -522,7 +524,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "testuser", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task in 31s**"); }); @@ -533,7 +535,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "testuser", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task**"); expect(result).not.toContain(" in "); }); @@ -553,7 +555,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "trigger-user", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); // Check header expect(result).toContain( @@ -594,7 +596,7 @@ describe("updateForgejoCommentBody", () => { prLink: "", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).toContain("Claude finished @claude's task in 1m 30s"); expect(result).toContain(`[View job](${forgejoBaseInput.jobUrl})`); @@ -612,7 +614,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "user", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); expect(result).not.toContain("[View job run]"); expect(result).toContain("[View job]"); }); @@ -626,7 +628,7 @@ describe("updateForgejoCommentBody", () => { triggerUsername: "user", }; - const result = updateForgejoCommentBody(input); + const result = updateCommentBody(input); // Old link in body removed expect(result).not.toContain("[View branch]"); // New branch in header diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 86e87ee..b5322b5 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -3,6 +3,7 @@ import { prepareMcpConfig } from "../src/mcp/install-mcp-server"; import * as core from "@actions/core"; import type { ParsedGitHubContext } from "../src/github/context"; import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants"; +import { _resetPlatformCache } from "../src/platform/detector"; describe("prepareMcpConfig", () => { let consoleInfoSpy: any; @@ -321,6 +322,7 @@ describe("prepareMcpConfig", () => { let savedServerUrl: string | undefined; beforeEach(() => { + _resetPlatformCache(); savedPlatform = process.env.PLATFORM; savedForgejo = process.env.FORGEJO; savedServerUrl = process.env.GITHUB_SERVER_URL; @@ -344,6 +346,7 @@ describe("prepareMcpConfig", () => { } else { delete process.env.GITHUB_SERVER_URL; } + _resetPlatformCache(); }); test("should skip GitHub public MCP server on Forgejo", async () => { diff --git a/test/m7-integration.test.ts b/test/m7-integration.test.ts index 35178ec..d717265 100644 --- a/test/m7-integration.test.ts +++ b/test/m7-integration.test.ts @@ -9,7 +9,7 @@ */ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { detectPlatform } from "../src/platform/detector"; +import { detectPlatform, _resetPlatformCache } from "../src/platform/detector"; // --------------------------------------------------------------------------- // Helpers @@ -40,11 +40,13 @@ function restoreEnv() { describe("M7: Platform routing for mode handlers", () => { beforeEach(() => { + _resetPlatformCache(); saveEnv("PLATFORM", "FORGEJO", "GITHUB_SERVER_URL"); }); afterEach(() => { restoreEnv(); + _resetPlatformCache(); }); describe("detectPlatform() — used by tag mode and update-comment-link", () => { @@ -156,13 +158,13 @@ describe("M7: Forgejo PR compare URL format", () => { // update-comment-link: platform-aware final comment body // --------------------------------------------------------------------------- -describe("M7: updateForgejoCommentBody vs updateCommentBody", () => { - it("Forgejo version strips Unicode spinner", async () => { - const { updateForgejoCommentBody } = await import( - "../src/forgejo/comment-logic" +describe("M7: unified updateCommentBody handles both platforms", () => { + it("strips Unicode spinner (Forgejo format)", async () => { + const { updateCommentBody } = await import( + "../src/github/operations/comment-logic" ); - const result = updateForgejoCommentBody({ + const result = updateCommentBody({ currentBody: "Claude Code is working… ⏳\n\nI'll analyze this and get back to you.\n\n[View job run](https://git.wxs.ro/owner/repo/actions/runs/123)", actionFailed: false, diff --git a/test/permissions.test.ts b/test/permissions.test.ts index 778b1c7..2ca5eee 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -3,6 +3,7 @@ import * as core from "@actions/core"; import { checkWritePermissions } from "../src/github/validation/permissions"; import type { ParsedGitHubContext } from "../src/github/context"; import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants"; +import { _resetPlatformCache } from "../src/platform/detector"; describe("checkWritePermissions", () => { let coreInfoSpy: any; @@ -310,6 +311,7 @@ describe("checkWritePermissions", () => { let savedServerUrl: string | undefined; beforeEach(() => { + _resetPlatformCache(); savedPlatform = process.env.PLATFORM; savedForgejo = process.env.FORGEJO; savedServerUrl = process.env.GITHUB_SERVER_URL; @@ -324,6 +326,7 @@ describe("checkWritePermissions", () => { if (savedServerUrl !== undefined) process.env.GITHUB_SERVER_URL = savedServerUrl; else delete process.env.GITHUB_SERVER_URL; + _resetPlatformCache(); }); const createForgejoOctokit = (permissions: { diff --git a/test/platform-detector.test.ts b/test/platform-detector.test.ts index 961dc5b..d3b1a30 100644 --- a/test/platform-detector.test.ts +++ b/test/platform-detector.test.ts @@ -1,9 +1,10 @@ -import { detectPlatform } from "../src/platform/detector"; +import { detectPlatform, _resetPlatformCache } from "../src/platform/detector"; import { describe, it, expect, beforeEach } from "bun:test"; describe("detectPlatform", () => { beforeEach(() => { - // Reset relevant env vars before each test + // Reset memoized result and env vars before each test + _resetPlatformCache(); delete process.env.PLATFORM; delete process.env.FORGEJO; delete process.env.GITHUB_SERVER_URL;