From b03ccb518ca6f7cdeeae08adb4e602d0e9ccb548 Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Wed, 25 Feb 2026 20:43:16 +0200 Subject: [PATCH 1/3] feat(prd-5): add dedicated claude-bot user for Forgejo - Add bot_email action input for custom git commit email on Forgejo - Update claude.yml to use CLAUDE_BOT_TOKEN with bot_id/bot_name/bot_email - Update git-config.ts to use provided email over noreply format - Add "Dedicated Bot User" setup guide to docs/forgejo.md - Update CLAUDE.md with bot identity documentation Infrastructure (done outside code): - claude-bot user created (ID 48, non-admin, with avatar) - Claude team with write access in all 12 orgs - CLAUDE_BOT_TOKEN org-level secret in all 12 orgs Progress: ~85% complete - pending E2E validation after merge Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/claude.yml | 1 + CLAUDE.md | 2 ++ action.yml | 5 ++++ docs/forgejo.md | 10 ++++---- prds/5-dedicated-forgejo-user.md | 39 ++++++++++++++++++++++------- src/github/context.ts | 2 ++ src/github/operations/git-config.ts | 6 +++-- src/modes/agent/index.ts | 2 ++ src/modes/tag/index.ts | 2 ++ test/install-mcp-server.test.ts | 1 + test/mockContext.ts | 1 + test/modes/detector.test.ts | 1 + test/permissions.test.ts | 1 + 13 files changed, 57 insertions(+), 16 deletions(-) diff --git a/.forgejo/workflows/claude.yml b/.forgejo/workflows/claude.yml index af9f441..a5c06c8 100644 --- a/.forgejo/workflows/claude.yml +++ b/.forgejo/workflows/claude.yml @@ -40,6 +40,7 @@ jobs: github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} bot_id: "48" bot_name: "claude-bot" + bot_email: "claude@wxs.ro" platform: "forgejo" env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/CLAUDE.md b/CLAUDE.md index ee7c1f5..eddc8a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,8 @@ This fork adds Forgejo support via a lean adapter pattern — only 4 genuinely d All other `src/github/` modules work on Forgejo via Octokit REST as-is (permissions, comments, branches, git config, context, formatter). +**Bot identity on Forgejo**: A dedicated `claude-bot` user provides bot identity (like GitHub's `claude[bot]`). The `bot_id`, `bot_name`, and `bot_email` action inputs control git commit attribution. When `bot_email` is set, it overrides the default noreply email format — set it to the bot user's registered email so Forgejo links commits to the profile. See `docs/forgejo.md` for setup. + **Key integration points** (these files have platform branches and are merge-conflict surface): - `src/entrypoints/run.ts` — Platform branch at top for token setup diff --git a/action.yml b/action.yml index 419bef3..83a99fb 100644 --- a/action.yml +++ b/action.yml @@ -105,6 +105,10 @@ inputs: description: "GitHub username to use for git operations (defaults to Claude's bot name)" required: false default: "claude[bot]" + bot_email: + description: "Email to use for git commits. When set, overrides the default noreply email format. Useful for Forgejo where matching the user's registered email links commits to the profile." + required: false + default: "" track_progress: description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events." required: false @@ -245,6 +249,7 @@ runs: SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }} BOT_ID: ${{ inputs.bot_id }} BOT_NAME: ${{ inputs.bot_name }} + BOT_EMAIL: ${{ inputs.bot_email }} TRACK_PROGRESS: ${{ inputs.track_progress }} INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }} ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} diff --git a/docs/forgejo.md b/docs/forgejo.md index 58b99c5..e1800dd 100644 --- a/docs/forgejo.md +++ b/docs/forgejo.md @@ -43,9 +43,9 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID (see step 5 below) + bot_id: "48" # Your bot user's Forgejo ID bot_name: "claude-bot" - bot_email: "claude@yourdomain.com" # Must match the bot user's registered email + bot_email: "claude@wxs.ro" ``` ### 3. Agent Mode (Automation) @@ -71,9 +71,9 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID + bot_id: "48" bot_name: "claude-bot" - bot_email: "claude@yourdomain.com" + bot_email: "claude@wxs.ro" ``` ## Platform Detection @@ -198,7 +198,7 @@ curl -s "https://your-forgejo-instance/api/v1/users/claude-bot" | jq '.id' anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: "YOUR_BOT_ID" # Replace with the ID from step 5 + bot_id: "48" # From step 5 bot_name: "claude-bot" bot_email: "claude@yourdomain.com" # Must match the bot user's registered email ``` diff --git a/prds/5-dedicated-forgejo-user.md b/prds/5-dedicated-forgejo-user.md index ed11cd0..dd8c868 100644 --- a/prds/5-dedicated-forgejo-user.md +++ b/prds/5-dedicated-forgejo-user.md @@ -3,6 +3,7 @@ **Issue**: [#5](https://git.wxs.ro/actions/claude-code-action/issues/5) **Priority**: High **Created**: 2026-02-25 +**Status**: ~85% complete — code and infra done, pending E2E validation after merge --- @@ -25,15 +26,16 @@ Create a dedicated `claude-bot` user on the Forgejo instance (git.wxs.ro), gener ### Phase 1: Forgejo Admin Setup -- [ ] **M1: Create dedicated `claude-bot` user on git.wxs.ro** +- [x] **M1: Create dedicated `claude-bot` user on git.wxs.ro** - Create user via Forgejo admin panel or API (`POST /admin/users`) - Set appropriate display name (e.g., "Claude Bot") - Set avatar to distinguish from human users - User should NOT have admin privileges - **Validation**: User exists and can log in + - **Evidence**: User ID 48, `is_admin: false`, avatar set to Claude logo -- [ ] **M2: Configure bot user permissions and token** +- [x] **M2: Configure bot user permissions and token** - Add `claude-bot` to the `actions` org (and any other orgs where the action runs) - Grant `write` permission on repos where Claude needs to operate - Generate a scoped API token for the bot user with minimal required permissions: @@ -41,30 +43,35 @@ Create a dedicated `claude-bot` user on the Forgejo instance (git.wxs.ro), gener - `packages` (if needed for container operations) - NO admin scope, NO user scope beyond what's needed - **Validation**: Token can create comments and push branches on a test repo + - **Evidence**: `Claude` team with `write` permission created in all 12 orgs, token validated (comment create/delete on issue #5, `push: true` on k8s/flux, crossplane/harbor-resources) ### Phase 2: Action Configuration -- [ ] **M3: Update workflow to use bot token** +- [x] **M3: Update workflow to use bot token** - Store the bot token as a Forgejo Actions secret (e.g., `CLAUDE_BOT_TOKEN`) - Update `claude.yml` workflow to pass `github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}` instead of the human token - Update `bot_id` and `bot_name` inputs to match the new Forgejo user's ID and username - Document the token rotation procedure - **Validation**: Action runs with bot token, comments appear as `claude-bot` + - **Evidence**: `CLAUDE_BOT_TOKEN` org secret in all 12 orgs, `claude.yml` updated with `bot_id: "48"`, `bot_name: "claude-bot"`, `bot_email: "claude@wxs.ro"`. New `bot_email` input added to `action.yml`. + - [ ] **Pending E2E**: Trigger a real `@claude` mention to verify comments appear as `claude-bot` -- [ ] **M4: Verify git commit attribution** +- [x] **M4: Verify git commit attribution** - Ensure `git config user.name` and `user.email` use the bot user's identity - Verify that `src/github/operations/git-config.ts` correctly picks up the bot user info from the token - Test that pushed commits show the bot user as author in Forgejo UI - **Validation**: A tag-mode response that creates a branch shows `claude-bot` as commit author + - **Evidence**: `git-config.ts` uses `user.email` when provided (falls back to noreply), `context.ts` reads `BOT_EMAIL` env var, all call sites pass `botEmail`. 794 tests pass. + - [ ] **Pending E2E**: Trigger a tag-mode response that creates a branch to verify commit authorship ### Phase 3: Documentation & Hardening -- [ ] **M5: Update documentation and test across repos** - - Update `docs/forgejo.md` with bot user setup instructions - - Document token rotation procedure (tokens expire, need renewal plan) - - Test the bot user setup on at least one additional repo beyond `claude-code-action` - - Update `CLAUDE.md` if any workflow changes affect development +- [~] **M5: Update documentation and test across repos** + - [x] Update `docs/forgejo.md` with bot user setup instructions (full 6-step guide added) + - [x] Document token rotation procedure (tokens expire, need renewal plan) + - [ ] Test the bot user setup on at least one additional repo beyond `claude-code-action` + - [x] Update `CLAUDE.md` if any workflow changes affect development - **Validation**: Another developer can follow the docs to set up the bot user on a new Forgejo instance ## Dependencies @@ -86,3 +93,17 @@ Create a dedicated `claude-bot` user on the Forgejo instance (git.wxs.ro), gener - All Claude comments and commits on Forgejo repos appear as `claude-bot`, not the human user - The bot token has minimal scoped permissions (no admin access) - Documentation allows reproducing the setup on a fresh Forgejo instance + +## Implementation Notes + +### New `bot_email` Action Input + +Added `bot_email` input to `action.yml` — when set, overrides the default noreply email format (`{id}+{name}@users.noreply.{host}`). On Forgejo, setting this to the bot user's registered email links commits to the bot's profile in the UI. + +### Org-Level Secrets + +`CLAUDE_BOT_TOKEN` is stored as an org-level secret (not repo-level) in all 12 orgs with `visibility: "all"`. This means any new repo in any org will automatically have access. + +### Team Structure + +Each org has a `Claude` team with `write` permission on all repositories (`includes_all_repositories: true`). New repos are automatically covered. diff --git a/src/github/context.ts b/src/github/context.ts index 936e70e..aca526b 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -94,6 +94,7 @@ type BaseContext = { sshSigningKey: string; botId: string; botName: string; + botEmail: string; allowedBots: string; allowedNonWriteUsers: string; trackProgress: boolean; @@ -154,6 +155,7 @@ export function parseGitHubContext(): GitHubContext { sshSigningKey: process.env.SSH_SIGNING_KEY || "", botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN, + botEmail: process.env.BOT_EMAIL ?? "", allowedBots: process.env.ALLOWED_BOTS ?? "", allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "", trackProgress: process.env.TRACK_PROGRESS === "true", diff --git a/src/github/operations/git-config.ts b/src/github/operations/git-config.ts index 97e02be..729eeda 100644 --- a/src/github/operations/git-config.ts +++ b/src/github/operations/git-config.ts @@ -17,6 +17,7 @@ const SSH_SIGNING_KEY_PATH = join(homedir(), ".ssh", "claude_signing_key"); type GitUser = { login: string; id: number; + email?: string; }; export async function configureGitAuth( @@ -37,10 +38,11 @@ export async function configureGitAuth( console.log("Configuring git user..."); const botName = user.login; const botId = user.id; + const botEmail = user.email || `${botId}+${botName}@${noreplyDomain}`; console.log(`Setting git user as ${botName}...`); await $`git config user.name "${botName}"`; - await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`; - console.log(`✓ Set git user as ${botName}`); + await $`git config user.email "${botEmail}"`; + console.log(`✓ Set git user as ${botName} <${botEmail}>`); // Remove the authorization header that actions/checkout sets console.log("Removing existing git authentication headers..."); diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts index e604737..f12628d 100644 --- a/src/modes/agent/index.ts +++ b/src/modes/agent/index.ts @@ -41,6 +41,7 @@ export async function prepareAgentMode({ const user = { login: context.inputs.botName, id: parseInt(context.inputs.botId), + email: context.inputs.botEmail || undefined, }; try { await configureGitAuth(githubToken, context, user); @@ -53,6 +54,7 @@ export async function prepareAgentMode({ const user = { login: context.inputs.botName, id: parseInt(context.inputs.botId), + email: context.inputs.botEmail || undefined, }; try { diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index 3aa16fe..f648b9a 100644 --- a/src/modes/tag/index.ts +++ b/src/modes/tag/index.ts @@ -86,6 +86,7 @@ export async function prepareTagMode({ const user = { login: context.inputs.botName, id: parseInt(context.inputs.botId), + email: context.inputs.botEmail || undefined, }; try { await configureGitAuth(githubToken, context, user); @@ -98,6 +99,7 @@ export async function prepareTagMode({ const user = { login: context.inputs.botName, id: parseInt(context.inputs.botId), + email: context.inputs.botEmail || undefined, }; try { diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 5e30cbd..86e87ee 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -36,6 +36,7 @@ describe("prepareMcpConfig", () => { sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, + botEmail: "", allowedBots: "", allowedNonWriteUsers: "", trackProgress: false, diff --git a/test/mockContext.ts b/test/mockContext.ts index 19ab044..8855132 100644 --- a/test/mockContext.ts +++ b/test/mockContext.ts @@ -23,6 +23,7 @@ const defaultInputs = { sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, + botEmail: "", allowedBots: "", allowedNonWriteUsers: "", trackProgress: false, diff --git a/test/modes/detector.test.ts b/test/modes/detector.test.ts index c8a6c75..436e646 100644 --- a/test/modes/detector.test.ts +++ b/test/modes/detector.test.ts @@ -23,6 +23,7 @@ describe("detectMode with enhanced routing", () => { sshSigningKey: "", botId: "123456", botName: "claude-bot", + botEmail: "", allowedBots: "", allowedNonWriteUsers: "", trackProgress: false, diff --git a/test/permissions.test.ts b/test/permissions.test.ts index 240356c..778b1c7 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -71,6 +71,7 @@ describe("checkWritePermissions", () => { sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), botName: CLAUDE_BOT_LOGIN, + botEmail: "", allowedBots: "", allowedNonWriteUsers: "", trackProgress: false, From 6ebfb4914ffbd63579bf58d0ab49b692ddf8a979 Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Wed, 25 Feb 2026 20:56:26 +0200 Subject: [PATCH 2/3] docs: replace instance-specific values with placeholders in forgejo.md Replace hardcoded bot_id "48" and bot_email "claude@wxs.ro" with generic placeholders in Quick Start and setup examples so readers don't copy instance-specific values. Co-Authored-By: Claude Opus 4.6 --- docs/forgejo.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/forgejo.md b/docs/forgejo.md index e1800dd..58b99c5 100644 --- a/docs/forgejo.md +++ b/docs/forgejo.md @@ -43,9 +43,9 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: "48" # Your bot user's Forgejo ID + bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID (see step 5 below) bot_name: "claude-bot" - bot_email: "claude@wxs.ro" + bot_email: "claude@yourdomain.com" # Must match the bot user's registered email ``` ### 3. Agent Mode (Automation) @@ -71,9 +71,9 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: "48" + bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID bot_name: "claude-bot" - bot_email: "claude@wxs.ro" + bot_email: "claude@yourdomain.com" ``` ## Platform Detection @@ -198,7 +198,7 @@ curl -s "https://your-forgejo-instance/api/v1/users/claude-bot" | jq '.id' anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} - bot_id: "48" # From step 5 + bot_id: "YOUR_BOT_ID" # Replace with the ID from step 5 bot_name: "claude-bot" bot_email: "claude@yourdomain.com" # Must match the bot user's registered email ``` From 23f15aecb942a822cba8f9f29448e2be684639df Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Wed, 25 Feb 2026 21:07:40 +0200 Subject: [PATCH 3/3] fix: use GITHUB_RUN_NUMBER for Forgejo job URLs and pin Bun version in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forgejo uses the per-repo sequential run index (GITHUB_RUN_NUMBER) in action URLs, not the internal DB run ID (GITHUB_RUN_ID). This caused "View job" links to return 404. Fixed in all 4 URL construction sites: create-initial comment, update-comment-link, and both prompt generators. Also pins Bun to 1.2.21 in CI — Bun 1.3.9 broke spyOn(fs, ...) which failed 18 upstream image-downloader tests. Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/ci.yml | 38 ++++++++++++++--- src/create-prompt/index.ts | 14 ++++++- src/entrypoints/update-comment-link.ts | 5 ++- src/forgejo/comment-logic.ts | 18 ++++++++ .../operations/comments/create-initial.ts | 3 +- test/forgejo-comment-logic.test.ts | 41 +++++++++++++++++++ 6 files changed, 109 insertions(+), 10 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 5d86aba..ab247ac 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -13,6 +13,11 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true +# Pin Bun version across all jobs. Bun 1.3.x broke spyOn(fs, ...) in tests. +# Bump this when upstream tests are updated for newer Bun versions. +env: + BUN_VERSION: "1.2.21" + jobs: test: runs-on: grunt @@ -25,9 +30,16 @@ jobs: - name: Install Bun run: | if command -v bun &> /dev/null; then - echo "Bun already installed: $(bun --version)" + INSTALLED=$(bun --version) + if [ "$INSTALLED" = "$BUN_VERSION" ]; then + echo "Bun $BUN_VERSION already installed" + else + echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" + echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + fi else - curl -fsSL https://bun.sh/install | bash + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" echo "$HOME/.bun/bin" >> "$GITHUB_PATH" fi @@ -56,9 +68,16 @@ jobs: - name: Install Bun run: | if command -v bun &> /dev/null; then - echo "Bun already installed: $(bun --version)" + INSTALLED=$(bun --version) + if [ "$INSTALLED" = "$BUN_VERSION" ]; then + echo "Bun $BUN_VERSION already installed" + else + echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" + echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + fi else - curl -fsSL https://bun.sh/install | bash + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" echo "$HOME/.bun/bin" >> "$GITHUB_PATH" fi @@ -79,9 +98,16 @@ jobs: - name: Install Bun run: | if command -v bun &> /dev/null; then - echo "Bun already installed: $(bun --version)" + INSTALLED=$(bun --version) + if [ "$INSTALLED" = "$BUN_VERSION" ]; then + echo "Bun $BUN_VERSION already installed" + else + echo "Bun $INSTALLED installed, need $BUN_VERSION — reinstalling" + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" + echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + fi else - curl -fsSL https://bun.sh/install | bash + curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" echo "$HOME/.bun/bin" >> "$GITHUB_PATH" fi diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 4624c19..d9f99cd 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -20,6 +20,8 @@ import { import type { ParsedGitHubContext } from "../github/context"; import type { CommonFields, PreparedContext, EventData } from "./types"; import { GITHUB_SERVER_URL } from "../github/api/config"; +import { detectPlatform } from "../platform/detector"; +import { getForgejoRunIndex } from "../forgejo/comment-logic"; import { extractUserRequest } from "../utils/extract-user-request"; export type { CommonFields, PreparedContext } from "./types"; @@ -525,7 +527,11 @@ Images from comments have been saved to disk. Paths are in the formatted content : "No description provided"; const entityType = eventData.isPR ? "pull request" : "issue"; - const jobUrl = `${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const runIdentifier = + detectPlatform() === "forgejo" + ? getForgejoRunIndex() + : process.env.GITHUB_RUN_ID; + const jobUrl = `${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${runIdentifier}`; let promptContent = `You were tagged on a GitHub ${entityType} via "${context.triggerPhrase}". Read the request and decide how to help. @@ -624,6 +630,10 @@ export function generateDefaultPrompt( const { eventData } = context; const { eventType, triggerContext } = getEventTypeAndContext(context); + const runIdentifier = + detectPlatform() === "forgejo" + ? getForgejoRunIndex() + : process.env.GITHUB_RUN_ID; const formattedContext = formatContext(contextData, eventData.isPR); const formattedComments = formatComments(comments, imageUrlMap); @@ -831,7 +841,7 @@ ${ - Display the todo list as a checklist in the GitHub comment and mark things off as you go. - REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively. - Use h3 headers (###) for section titles in your comments, not h1 headers (#). -- Your comment must always include the job run link in the format "[View job run](${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID})" at the bottom of your response (branch link if there is one should also be included there). +- Your comment must always include the job run link in the format "[View job run](${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${runIdentifier})" at the bottom of your response (branch link if there is one should also be included there). CAPABILITIES AND LIMITATIONS: When users ask you to do something, be aware of what you can and cannot do. This section helps you understand how to respond when users request actions outside your scope. diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 3dc6e71..40f1c54 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -10,6 +10,7 @@ import { import { updateForgejoCommentBody, createForgejoPrCompareUrl, + getForgejoRunIndex, } from "../forgejo/comment-logic"; import { detectPlatform } from "../platform/detector"; import { @@ -54,7 +55,9 @@ export async function updateCommentLink( const platform = detectPlatform(); const serverUrl = GITHUB_SERVER_URL; - const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const runIdentifier = + platform === "forgejo" ? getForgejoRunIndex() : process.env.GITHUB_RUN_ID; + const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${runIdentifier}`; let comment; let isPRReviewComment = false; diff --git a/src/forgejo/comment-logic.ts b/src/forgejo/comment-logic.ts index 9b83b9a..2fccefb 100644 --- a/src/forgejo/comment-logic.ts +++ b/src/forgejo/comment-logic.ts @@ -34,6 +34,24 @@ function getServerUrl(): string { return process.env.GITHUB_SERVER_URL || "https://github.com"; } +// --------------------------------------------------------------------------- +// Run index helper +// --------------------------------------------------------------------------- + +/** + * Get the run identifier for Forgejo URLs. + * + * Forgejo uses the per-repo sequential run index in URLs (`/actions/runs/{index}`), + * not the internal database run ID. The Forgejo runner sets: + * - GITHUB_RUN_ID = internal DB primary key (e.g. 11943) + * - GITHUB_RUN_NUMBER = per-repo sequential index (e.g. 661) + * + * GitHub uses GITHUB_RUN_ID in URLs, but Forgejo uses GITHUB_RUN_NUMBER. + */ +export function getForgejoRunIndex(): string { + return process.env.GITHUB_RUN_NUMBER || process.env.GITHUB_RUN_ID || "0"; +} + // --------------------------------------------------------------------------- // Spinner // --------------------------------------------------------------------------- diff --git a/src/github/operations/comments/create-initial.ts b/src/github/operations/comments/create-initial.ts index 5ee0024..dd678c0 100644 --- a/src/github/operations/comments/create-initial.ts +++ b/src/github/operations/comments/create-initial.ts @@ -10,6 +10,7 @@ import { createJobRunLink, createCommentBody } from "./common"; import { createForgejoJobRunLink, createForgejoCommentBody, + getForgejoRunIndex, } from "../../../forgejo/comment-logic"; import { detectPlatform } from "../../../platform/detector"; import { @@ -30,7 +31,7 @@ export async function createInitialComment( const jobRunLink = platform === "forgejo" - ? createForgejoJobRunLink(owner, repo, context.runId) + ? createForgejoJobRunLink(owner, repo, getForgejoRunIndex()) : createJobRunLink(owner, repo, context.runId); const initialBody = platform === "forgejo" diff --git a/test/forgejo-comment-logic.test.ts b/test/forgejo-comment-logic.test.ts index e77b8dc..093bfe8 100644 --- a/test/forgejo-comment-logic.test.ts +++ b/test/forgejo-comment-logic.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { FORGEJO_SPINNER_HTML, + getForgejoRunIndex, createForgejoJobRunLink, createForgejoBranchLink, createForgejoPrCompareUrl, @@ -37,6 +38,46 @@ describe("FORGEJO_SPINNER_HTML", () => { }); }); +// --------------------------------------------------------------------------- +// getForgejoRunIndex +// --------------------------------------------------------------------------- + +describe("getForgejoRunIndex", () => { + const origRunNumber = process.env.GITHUB_RUN_NUMBER; + const origRunId = process.env.GITHUB_RUN_ID; + + afterEach(() => { + if (origRunNumber !== undefined) { + process.env.GITHUB_RUN_NUMBER = origRunNumber; + } else { + delete process.env.GITHUB_RUN_NUMBER; + } + if (origRunId !== undefined) { + process.env.GITHUB_RUN_ID = origRunId; + } else { + delete process.env.GITHUB_RUN_ID; + } + }); + + it("prefers GITHUB_RUN_NUMBER over GITHUB_RUN_ID", () => { + process.env.GITHUB_RUN_NUMBER = "661"; + process.env.GITHUB_RUN_ID = "11943"; + expect(getForgejoRunIndex()).toBe("661"); + }); + + it("falls back to GITHUB_RUN_ID when GITHUB_RUN_NUMBER is not set", () => { + delete process.env.GITHUB_RUN_NUMBER; + process.env.GITHUB_RUN_ID = "11943"; + expect(getForgejoRunIndex()).toBe("11943"); + }); + + it("returns '0' when neither env var is set", () => { + delete process.env.GITHUB_RUN_NUMBER; + delete process.env.GITHUB_RUN_ID; + expect(getForgejoRunIndex()).toBe("0"); + }); +}); + // --------------------------------------------------------------------------- // createForgejoJobRunLink // ---------------------------------------------------------------------------