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 <noreply@anthropic.com>
This commit is contained in:
Vlad MocanuandClaude Opus 4.6 committed 2026-02-25 20:56:55 +02:00
1 parent 07e0384538
commit b03ccb518c
13 files changed
+57 -16

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ jobs:
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}
bot_id: "48" bot_id: "48"
bot_name: "claude-bot" bot_name: "claude-bot"
bot_email: "claude@wxs.ro"
platform: "forgejo" platform: "forgejo"
env: env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+2
View File
@@ -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). 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): **Key integration points** (these files have platform branches and are merge-conflict surface):
- `src/entrypoints/run.ts` — Platform branch at top for token setup - `src/entrypoints/run.ts` — Platform branch at top for token setup
+5
View File
@@ -105,6 +105,10 @@ inputs:
description: "GitHub username to use for git operations (defaults to Claude's bot name)" description: "GitHub username to use for git operations (defaults to Claude's bot name)"
required: false required: false
default: "claude[bot]" 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: 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." 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 required: false
@@ -245,6 +249,7 @@ runs:
SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }} SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }}
BOT_ID: ${{ inputs.bot_id }} BOT_ID: ${{ inputs.bot_id }}
BOT_NAME: ${{ inputs.bot_name }} BOT_NAME: ${{ inputs.bot_name }}
BOT_EMAIL: ${{ inputs.bot_email }}
TRACK_PROGRESS: ${{ inputs.track_progress }} TRACK_PROGRESS: ${{ inputs.track_progress }}
INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }} INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
+5 -5
View File
@@ -43,9 +43,9 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
platform: "forgejo" platform: "forgejo"
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} 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_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) ### 3. Agent Mode (Automation)
@@ -71,9 +71,9 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
platform: "forgejo" platform: "forgejo"
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} 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_name: "claude-bot"
bot_email: "claude@yourdomain.com" bot_email: "claude@wxs.ro"
``` ```
## Platform Detection ## 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 }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
platform: "forgejo" platform: "forgejo"
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} 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_name: "claude-bot"
bot_email: "claude@yourdomain.com" # Must match the bot user's registered email bot_email: "claude@yourdomain.com" # Must match the bot user's registered email
``` ```
+30 -9
View File
@@ -3,6 +3,7 @@
**Issue**: [#5](https://git.wxs.ro/actions/claude-code-action/issues/5) **Issue**: [#5](https://git.wxs.ro/actions/claude-code-action/issues/5)
**Priority**: High **Priority**: High
**Created**: 2026-02-25 **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 ### 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`) - Create user via Forgejo admin panel or API (`POST /admin/users`)
- Set appropriate display name (e.g., "Claude Bot") - Set appropriate display name (e.g., "Claude Bot")
- Set avatar to distinguish from human users - Set avatar to distinguish from human users
- User should NOT have admin privileges - User should NOT have admin privileges
- **Validation**: User exists and can log in - **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) - 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 - Grant `write` permission on repos where Claude needs to operate
- Generate a scoped API token for the bot user with minimal required permissions: - 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) - `packages` (if needed for container operations)
- NO admin scope, NO user scope beyond what's needed - NO admin scope, NO user scope beyond what's needed
- **Validation**: Token can create comments and push branches on a test repo - **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 ### 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`) - 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 `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 - Update `bot_id` and `bot_name` inputs to match the new Forgejo user's ID and username
- Document the token rotation procedure - Document the token rotation procedure
- **Validation**: Action runs with bot token, comments appear as `claude-bot` - **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 - 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 - 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 - 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 - **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 ### Phase 3: Documentation & Hardening
- [ ] **M5: Update documentation and test across repos** - [~] **M5: Update documentation and test across repos**
- Update `docs/forgejo.md` with bot user setup instructions - [x] Update `docs/forgejo.md` with bot user setup instructions (full 6-step guide added)
- Document token rotation procedure (tokens expire, need renewal plan) - [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` - [ ] Test the bot user setup on at least one additional repo beyond `claude-code-action`
- Update `CLAUDE.md` if any workflow changes affect development - [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 - **Validation**: Another developer can follow the docs to set up the bot user on a new Forgejo instance
## Dependencies ## 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 - 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) - The bot token has minimal scoped permissions (no admin access)
- Documentation allows reproducing the setup on a fresh Forgejo instance - 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.
+2
View File
@@ -94,6 +94,7 @@ type BaseContext = {
sshSigningKey: string; sshSigningKey: string;
botId: string; botId: string;
botName: string; botName: string;
botEmail: string;
allowedBots: string; allowedBots: string;
allowedNonWriteUsers: string; allowedNonWriteUsers: string;
trackProgress: boolean; trackProgress: boolean;
@@ -154,6 +155,7 @@ export function parseGitHubContext(): GitHubContext {
sshSigningKey: process.env.SSH_SIGNING_KEY || "", sshSigningKey: process.env.SSH_SIGNING_KEY || "",
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN, botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
botEmail: process.env.BOT_EMAIL ?? "",
allowedBots: process.env.ALLOWED_BOTS ?? "", allowedBots: process.env.ALLOWED_BOTS ?? "",
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "", allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
trackProgress: process.env.TRACK_PROGRESS === "true", trackProgress: process.env.TRACK_PROGRESS === "true",
+4 -2
View File
@@ -17,6 +17,7 @@ const SSH_SIGNING_KEY_PATH = join(homedir(), ".ssh", "claude_signing_key");
type GitUser = { type GitUser = {
login: string; login: string;
id: number; id: number;
email?: string;
}; };
export async function configureGitAuth( export async function configureGitAuth(
@@ -37,10 +38,11 @@ export async function configureGitAuth(
console.log("Configuring git user..."); console.log("Configuring git user...");
const botName = user.login; const botName = user.login;
const botId = user.id; const botId = user.id;
const botEmail = user.email || `${botId}+${botName}@${noreplyDomain}`;
console.log(`Setting git user as ${botName}...`); console.log(`Setting git user as ${botName}...`);
await $`git config user.name "${botName}"`; await $`git config user.name "${botName}"`;
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`; await $`git config user.email "${botEmail}"`;
console.log(`✓ Set git user as ${botName}`); console.log(`✓ Set git user as ${botName} <${botEmail}>`);
// Remove the authorization header that actions/checkout sets // Remove the authorization header that actions/checkout sets
console.log("Removing existing git authentication headers..."); console.log("Removing existing git authentication headers...");
+2
View File
@@ -41,6 +41,7 @@ export async function prepareAgentMode({
const user = { const user = {
login: context.inputs.botName, login: context.inputs.botName,
id: parseInt(context.inputs.botId), id: parseInt(context.inputs.botId),
email: context.inputs.botEmail || undefined,
}; };
try { try {
await configureGitAuth(githubToken, context, user); await configureGitAuth(githubToken, context, user);
@@ -53,6 +54,7 @@ export async function prepareAgentMode({
const user = { const user = {
login: context.inputs.botName, login: context.inputs.botName,
id: parseInt(context.inputs.botId), id: parseInt(context.inputs.botId),
email: context.inputs.botEmail || undefined,
}; };
try { try {
+2
View File
@@ -86,6 +86,7 @@ export async function prepareTagMode({
const user = { const user = {
login: context.inputs.botName, login: context.inputs.botName,
id: parseInt(context.inputs.botId), id: parseInt(context.inputs.botId),
email: context.inputs.botEmail || undefined,
}; };
try { try {
await configureGitAuth(githubToken, context, user); await configureGitAuth(githubToken, context, user);
@@ -98,6 +99,7 @@ export async function prepareTagMode({
const user = { const user = {
login: context.inputs.botName, login: context.inputs.botName,
id: parseInt(context.inputs.botId), id: parseInt(context.inputs.botId),
email: context.inputs.botEmail || undefined,
}; };
try { try {
+1
View File
@@ -36,6 +36,7 @@ describe("prepareMcpConfig", () => {
sshSigningKey: "", sshSigningKey: "",
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
botEmail: "",
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "", allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
+1
View File
@@ -23,6 +23,7 @@ const defaultInputs = {
sshSigningKey: "", sshSigningKey: "",
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
botEmail: "",
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "", allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
+1
View File
@@ -23,6 +23,7 @@ describe("detectMode with enhanced routing", () => {
sshSigningKey: "", sshSigningKey: "",
botId: "123456", botId: "123456",
botName: "claude-bot", botName: "claude-bot",
botEmail: "",
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "", allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
+1
View File
@@ -71,6 +71,7 @@ describe("checkWritePermissions", () => {
sshSigningKey: "", sshSigningKey: "",
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
botEmail: "",
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "", allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,