fix(prd-7): M1 architecture review — consolidate duplication, fix bugs, memoize detection
- F1: Consolidated updateForgejoCommentBody into unified updateCommentBody (~150 lines removed) - F2: Fixed wrong run identifier in update-with-branch.ts (context.runId → getForgejoRunIndex()) - F3: Memoized detectPlatform() with _resetPlatformCache() for tests - F4: Added getServerUrl() runtime helper to resolve constant-vs-function inconsistency - F5: Fixed sticky comment bot matching on Forgejo via context.inputs.botName - F6: Documented GHES heuristic misidentification in docs/forgejo.md - Updated PRD M1 checkbox to complete Progress: 17% complete — M1 done, M2 (error handling review) next Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae326659b1
commit
ac58fb6c14
15 files changed
+151
-266
No files matched your search
@@ -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*<img[^>]*>)?/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();
|
||||
}
|
||||
Reference in new issue
Block a user