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:
1 parent
07e0384538
commit
b03ccb518c
13 files changed
+57
-16
No files matched your search
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
+5
-5
@@ -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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -36,6 +36,7 @@ describe("prepareMcpConfig", () => {
|
||||
sshSigningKey: "",
|
||||
botId: String(CLAUDE_APP_BOT_ID),
|
||||
botName: CLAUDE_BOT_LOGIN,
|
||||
botEmail: "",
|
||||
allowedBots: "",
|
||||
allowedNonWriteUsers: "",
|
||||
trackProgress: false,
|
||||
|
||||
@@ -23,6 +23,7 @@ const defaultInputs = {
|
||||
sshSigningKey: "",
|
||||
botId: String(CLAUDE_APP_BOT_ID),
|
||||
botName: CLAUDE_BOT_LOGIN,
|
||||
botEmail: "",
|
||||
allowedBots: "",
|
||||
allowedNonWriteUsers: "",
|
||||
trackProgress: false,
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("detectMode with enhanced routing", () => {
|
||||
sshSigningKey: "",
|
||||
botId: "123456",
|
||||
botName: "claude-bot",
|
||||
botEmail: "",
|
||||
allowedBots: "",
|
||||
allowedNonWriteUsers: "",
|
||||
trackProgress: false,
|
||||
|
||||
@@ -71,6 +71,7 @@ describe("checkWritePermissions", () => {
|
||||
sshSigningKey: "",
|
||||
botId: String(CLAUDE_APP_BOT_ID),
|
||||
botName: CLAUDE_BOT_LOGIN,
|
||||
botEmail: "",
|
||||
allowedBots: "",
|
||||
allowedNonWriteUsers: "",
|
||||
trackProgress: false,
|
||||
|
||||
Reference in new issue
Block a user