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 }}
|
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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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...");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user