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;