mirror of
https://github.com/claude-did-this/claude-hub.git
synced 2026-02-14 19:30:02 +01:00
Compare commits
159 Commits
develop
...
chore/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58734fa62c | ||
|
|
9e5b3c3d20 | ||
|
|
bf1c42f5ca | ||
|
|
f765e2ac3e | ||
|
|
14785b2e64 | ||
|
|
faa60f4f55 | ||
|
|
4ece2969b3 | ||
|
|
295c182351 | ||
|
|
af851491e8 | ||
|
|
31efbbc2bb | ||
|
|
2e5fa7aa26 | ||
|
|
caad85d7a0 | ||
|
|
acf44b1c63 | ||
|
|
e463f2e5c5 | ||
|
|
150626b171 | ||
|
|
b028502a82 | ||
|
|
12e4589169 | ||
|
|
53d77c2856 | ||
|
|
df756e15ae | ||
|
|
f7399f8ad1 | ||
|
|
6b05644731 | ||
|
|
c837f36463 | ||
|
|
67e90c4b87 | ||
|
|
bddfc70f20 | ||
|
|
ddd5f97f8a | ||
|
|
cb1329d512 | ||
|
|
6cfbc0721c | ||
|
|
f5f7520588 | ||
|
|
41903540ea | ||
|
|
b23c5b1942 | ||
|
|
f42017f2a5 | ||
|
|
1c4cc39209 | ||
|
|
a40da0267e | ||
|
|
0035b7cac8 | ||
|
|
62ee5f4917 | ||
|
|
6b319fa511 | ||
|
|
e7f19d8307 | ||
|
|
a71cdcad40 | ||
|
|
cee3cd29f6 | ||
|
|
bac1583b46 | ||
|
|
e095826e02 | ||
|
|
426ac442e2 | ||
|
|
25b90a5d7c | ||
|
|
a45b039777 | ||
|
|
0169f338b0 | ||
|
|
d284bd6b33 | ||
|
|
cb5a6bf529 | ||
|
|
886544b1ad | ||
|
|
bda604bfdc | ||
|
|
f27009af37 | ||
|
|
57608e021b | ||
|
|
9339e5f87b | ||
|
|
348dfa6544 | ||
|
|
9c8276b92f | ||
|
|
223587a5aa | ||
|
|
a96b184357 | ||
|
|
30f24218ae | ||
|
|
210aa1f748 | ||
|
|
7039d07d29 | ||
|
|
02be8fc307 | ||
|
|
2101cd3450 | ||
|
|
c4575b7343 | ||
|
|
fe8b328e22 | ||
|
|
b260a7f559 | ||
|
|
3a56ee0499 | ||
|
|
2f7a2267bf | ||
|
|
6de92d9625 | ||
|
|
fdf255cbec | ||
|
|
3128a83b7a | ||
|
|
5fa329be9f | ||
|
|
f2b2224693 | ||
|
|
ea46c4329e | ||
|
|
d5755681b3 | ||
|
|
2739babc9a | ||
|
|
e8b09f0ee3 | ||
|
|
55a32bfbf3 | ||
|
|
eebbb450a4 | ||
|
|
f0a338d29f | ||
|
|
76141a7bf3 | ||
|
|
a6383dacf1 | ||
|
|
d88daa22f8 | ||
|
|
38c1ae5d61 | ||
|
|
0c3b0512c7 | ||
|
|
2bd9a02de1 | ||
|
|
30401a93c6 | ||
|
|
bbffefc248 | ||
|
|
3bb2dfda12 | ||
|
|
8906d7ce56 | ||
|
|
2011055fe2 | ||
|
|
7e654f9d13 | ||
|
|
a38ed85924 | ||
|
|
d20f9eec2d | ||
|
|
9498935eb8 | ||
|
|
c64c23d881 | ||
|
|
7d1043d54d | ||
|
|
b3be28ab6a | ||
|
|
b499bea1b4 | ||
|
|
407357e605 | ||
|
|
6d73b9848c | ||
|
|
08e4e66287 | ||
|
|
478916aa70 | ||
|
|
8788a87ff6 | ||
|
|
8b89ce741f | ||
|
|
b88cffe649 | ||
|
|
973bba5a8e | ||
|
|
6bdfad10cb | ||
|
|
f6281eb311 | ||
|
|
2f62c1529c | ||
|
|
a514de77b3 | ||
|
|
b048b1db58 | ||
|
|
f812b05639 | ||
|
|
7caa4d8f83 | ||
|
|
d5d5ca4d39 | ||
|
|
0b7d6f8e72 | ||
|
|
59b3850129 | ||
|
|
c53708b7be | ||
|
|
0e4d22bcdc | ||
|
|
52329e2fc9 | ||
|
|
d1a3917eb0 | ||
|
|
b6ee84193e | ||
|
|
aac286c281 | ||
|
|
a6feddd567 | ||
|
|
4338059113 | ||
|
|
aa66cdb29d | ||
|
|
24d849cedd | ||
|
|
145668dc74 | ||
|
|
29de1828fd | ||
|
|
48825c9415 | ||
|
|
b5c4920e6d | ||
|
|
d588c49b42 | ||
|
|
0ebcb41c2a | ||
|
|
86ffee346c | ||
|
|
70da142cf7 | ||
|
|
20667dd0cc | ||
|
|
0cf856b13c | ||
|
|
2750659801 | ||
|
|
82cca4b8c1 | ||
|
|
472b3b51be | ||
|
|
e1b72d76ae | ||
|
|
7fc4ad7c57 | ||
|
|
cb4628fb1f | ||
|
|
4d9834db7c | ||
|
|
8e2e30e38b | ||
|
|
582c785a67 | ||
|
|
00beec1269 | ||
|
|
78627ddeca | ||
|
|
b0abb63d88 | ||
|
|
ba2ad3587b | ||
|
|
6023380504 | ||
|
|
9867f6463d | ||
|
|
59a7a975be | ||
|
|
b0e5d01f6e | ||
|
|
4e318199b7 | ||
|
|
52018b9b17 | ||
|
|
3aeb53f2cc | ||
|
|
a77cda9c90 | ||
|
|
1f2c933076 | ||
|
|
d9b882846f | ||
|
|
64676d125f |
29
.codecov.yml
Normal file
29
.codecov.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
||||
base: auto
|
||||
# Only check coverage on main branch
|
||||
if_ci_failed: error
|
||||
patch:
|
||||
default:
|
||||
target: 50% # Lower diff coverage threshold - many changes are config/setup
|
||||
threshold: 15% # Allow 15% variance for diff coverage
|
||||
base: auto
|
||||
# Only check coverage on main branch
|
||||
if_ci_failed: error
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,tree"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
|
||||
github_checks:
|
||||
# Disable check suites to prevent hanging on non-main branches
|
||||
annotations: false
|
||||
75
.dockerignore
Normal file
75
.dockerignore
Normal file
@@ -0,0 +1,75 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
test-results
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Development
|
||||
.husky
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
!CLAUDE.md
|
||||
!README.dockerhub.md
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
!.github/workflows
|
||||
|
||||
# Secrets
|
||||
secrets
|
||||
CLAUDE.local.md
|
||||
|
||||
# Kubernetes
|
||||
k8s
|
||||
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
!docker-compose.test.yml
|
||||
Dockerfile*
|
||||
!Dockerfile
|
||||
!Dockerfile.claudecode
|
||||
.dockerignore
|
||||
|
||||
# Scripts - exclude all by default for security, then explicitly include needed runtime scripts
|
||||
*.sh
|
||||
!scripts/runtime/*.sh
|
||||
|
||||
# Test files (keep for test stage)
|
||||
# Removed test exclusion to allow test stage to access tests
|
||||
|
||||
# Build artifacts
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# Cache
|
||||
.cache
|
||||
.buildx-cache*
|
||||
tmp
|
||||
temp
|
||||
74
.env.example
74
.env.example
@@ -2,6 +2,32 @@
|
||||
NODE_ENV=development
|
||||
PORT=3002
|
||||
|
||||
# Trust Proxy Configuration
|
||||
# Set to 'true' when running behind reverse proxies (nginx, cloudflare, etc.)
|
||||
# This allows proper handling of X-Forwarded-For headers for rate limiting
|
||||
TRUST_PROXY=false
|
||||
|
||||
# ============================
|
||||
# SECRETS CONFIGURATION
|
||||
# ============================
|
||||
# The application supports two methods for providing secrets:
|
||||
#
|
||||
# 1. Environment Variables (shown below) - Convenient for development
|
||||
# 2. Secret Files - More secure for production
|
||||
#
|
||||
# If both are provided, SECRET FILES TAKE PRIORITY over environment variables.
|
||||
#
|
||||
# For file-based secrets, the app looks for files at:
|
||||
# - /run/secrets/github_token (or path in GITHUB_TOKEN_FILE)
|
||||
# - /run/secrets/anthropic_api_key (or path in ANTHROPIC_API_KEY_FILE)
|
||||
# - /run/secrets/webhook_secret (or path in GITHUB_WEBHOOK_SECRET_FILE)
|
||||
#
|
||||
# To use file-based secrets in development:
|
||||
# 1. Create a secrets directory: mkdir secrets
|
||||
# 2. Add secret files: echo "your-secret" > secrets/github_token.txt
|
||||
# 3. Mount in docker-compose or use GITHUB_TOKEN_FILE=/path/to/secret
|
||||
# ============================
|
||||
|
||||
# GitHub Webhook Settings
|
||||
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
GITHUB_TOKEN=ghp_your_github_token_here
|
||||
@@ -22,13 +48,27 @@ DEFAULT_BRANCH=main
|
||||
# Claude API Settings
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
|
||||
# Claude Hub Directory
|
||||
# Directory where Claude Hub stores configuration, authentication, and database files (default: ~/.claude-hub)
|
||||
CLAUDE_HUB_DIR=/home/user/.claude-hub
|
||||
|
||||
# Container Settings
|
||||
CLAUDE_USE_CONTAINERS=1
|
||||
CLAUDE_CONTAINER_IMAGE=claude-code-runner:latest
|
||||
CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
CLAUDE_CONTAINER_PRIVILEGED=false
|
||||
REPO_CACHE_DIR=/tmp/repo-cache
|
||||
REPO_CACHE_MAX_AGE_MS=3600000
|
||||
CONTAINER_LIFETIME_MS=7200000 # Container execution timeout in milliseconds (default: 2 hours)
|
||||
|
||||
# Claude Code Timeout Settings (for unattended mode)
|
||||
BASH_DEFAULT_TIMEOUT_MS=600000 # Default timeout for bash commands (10 minutes)
|
||||
BASH_MAX_TIMEOUT_MS=1200000 # Maximum timeout Claude can set (20 minutes)
|
||||
|
||||
# Container Resource Limits
|
||||
CLAUDE_CONTAINER_CPU_SHARES=1024
|
||||
CLAUDE_CONTAINER_MEMORY_LIMIT=2g
|
||||
CLAUDE_CONTAINER_PIDS_LIMIT=256
|
||||
|
||||
# AWS Bedrock Credentials for Claude (if using Bedrock)
|
||||
AWS_ACCESS_KEY_ID=your_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
|
||||
@@ -40,5 +80,35 @@ ANTHROPIC_MODEL=us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
# USE_AWS_PROFILE=true
|
||||
# AWS_PROFILE=claude-webhook
|
||||
|
||||
|
||||
# Container Capabilities (optional)
|
||||
CLAUDE_CONTAINER_CAP_NET_RAW=true
|
||||
CLAUDE_CONTAINER_CAP_SYS_TIME=false
|
||||
CLAUDE_CONTAINER_CAP_DAC_OVERRIDE=true
|
||||
CLAUDE_CONTAINER_CAP_AUDIT_WRITE=true
|
||||
CLAUDE_CONTAINER_CAP_SYS_ADMIN=false
|
||||
|
||||
# PR Review Configuration
|
||||
PR_REVIEW_WAIT_FOR_ALL_CHECKS=true
|
||||
PR_REVIEW_TRIGGER_WORKFLOW=Pull Request CI
|
||||
PR_REVIEW_DEBOUNCE_MS=5000
|
||||
PR_REVIEW_MAX_WAIT_MS=1800000
|
||||
PR_REVIEW_CONDITIONAL_TIMEOUT_MS=300000
|
||||
|
||||
# Test Configuration
|
||||
TEST_REPO_FULL_NAME=owner/repo
|
||||
TEST_REPO_FULL_NAME=owner/repo
|
||||
|
||||
# Security Configuration (optional)
|
||||
# DISABLE_LOG_REDACTION=false # WARNING: Only enable for debugging, exposes sensitive data in logs
|
||||
|
||||
# File-based Secrets (optional, takes priority over environment variables)
|
||||
# GITHUB_TOKEN_FILE=/run/secrets/github_token
|
||||
# ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
|
||||
# GITHUB_WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
|
||||
|
||||
# Authentication Methods (optional)
|
||||
# CLAUDE_AUTH_HOST_DIR=/path/to/claude/auth # For setup container authentication
|
||||
|
||||
# CLI Configuration (optional)
|
||||
# API_URL=http://localhost:3003 # Default API URL for CLI tool
|
||||
# WEBHOOK_URL=http://localhost:3002/api/webhooks/github # Webhook endpoint URL
|
||||
|
||||
25
.env.quickstart
Normal file
25
.env.quickstart
Normal file
@@ -0,0 +1,25 @@
|
||||
# Claude GitHub Webhook - Quick Start Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
#
|
||||
# cp .env.quickstart .env
|
||||
#
|
||||
# Only the essentials to get up and running in 10 minutes
|
||||
|
||||
# GitHub Configuration (Required)
|
||||
GITHUB_TOKEN=ghp_your_github_token_here
|
||||
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
|
||||
# Bot Identity (Required)
|
||||
BOT_USERNAME=@YourBotName
|
||||
BOT_EMAIL=bot@example.com
|
||||
|
||||
# Security - Who can use the bot
|
||||
AUTHORIZED_USERS=your-github-username
|
||||
DEFAULT_AUTHORIZED_USER=your-github-username
|
||||
|
||||
# Port (default: 3002)
|
||||
PORT=3002
|
||||
|
||||
# That's it! The setup script will handle Claude authentication.
|
||||
# Run: ./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
238
.github/CLAUDE.md
vendored
Normal file
238
.github/CLAUDE.md
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
# CI/CD Guidelines and Standards
|
||||
|
||||
This document defines the standards and best practices for our CI/CD pipelines. All workflows must adhere to these guidelines to ensure production-quality, maintainable, and secure automation.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Security First**: Never expose secrets, use least privilege, scan for vulnerabilities
|
||||
2. **Efficiency**: Minimize build times, use caching effectively, avoid redundant work
|
||||
3. **Reliability**: Proper error handling, clear failure messages, rollback capabilities
|
||||
4. **Maintainability**: DRY principles, clear naming, comprehensive documentation
|
||||
5. **Observability**: Detailed logs, status reporting, metrics collection
|
||||
|
||||
## Workflow Standards
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Workflow files**: Use kebab-case (e.g., `deploy-production.yml`)
|
||||
- **Workflow names**: Use title case (e.g., `Deploy to Production`)
|
||||
- **Job names**: Use descriptive names without redundancy (e.g., `test`, not `test-job`)
|
||||
- **Step names**: Start with verb, be specific (e.g., `Build Docker image`, not `Build`)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```yaml
|
||||
env:
|
||||
# Use repository variables with fallbacks
|
||||
DOCKER_REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }}
|
||||
APP_NAME: ${{ vars.APP_NAME || github.event.repository.name }}
|
||||
|
||||
# Never hardcode:
|
||||
# - URLs (use vars.PRODUCTION_URL)
|
||||
# - Usernames (use vars.DOCKER_USERNAME)
|
||||
# - Organization names (use vars.ORG_NAME)
|
||||
# - Ports (use vars.APP_PORT)
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
branches: [main] # Production deployments
|
||||
tags: ['v*.*.*'] # Semantic version releases
|
||||
pull_request:
|
||||
branches: [main, develop] # CI checks only, no deployments
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
1. **Permissions**: Always specify minimum required permissions
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
```
|
||||
|
||||
2. **Secret Handling**: Never create .env files with secrets
|
||||
```yaml
|
||||
# BAD - Exposes secrets in logs
|
||||
- run: echo "API_KEY=${{ secrets.API_KEY }}" > .env
|
||||
|
||||
# GOOD - Use GitHub's environment files
|
||||
- run: echo "API_KEY=${{ secrets.API_KEY }}" >> $GITHUB_ENV
|
||||
```
|
||||
|
||||
3. **Credential Scanning**: All workflows must pass credential scanning
|
||||
```yaml
|
||||
- name: Scan for credentials
|
||||
run: ./scripts/security/credential-audit.sh
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Deployment Scripts**: Always include error handling
|
||||
```yaml
|
||||
- name: Deploy application
|
||||
run: |
|
||||
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
||||
|
||||
./deploy.sh || {
|
||||
echo "::error::Deployment failed"
|
||||
./rollback.sh
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
2. **Health Checks**: Verify deployments succeeded
|
||||
```yaml
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
if curl -f "${{ vars.APP_URL }}/health"; then
|
||||
echo "Deployment successful"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::Health check failed after 5 minutes"
|
||||
exit 1
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
1. **Dependencies**: Use built-in caching
|
||||
```yaml
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: package-lock.json
|
||||
```
|
||||
|
||||
2. **Docker Builds**: Use GitHub Actions cache
|
||||
```yaml
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
```
|
||||
|
||||
### Docker Builds
|
||||
|
||||
1. **Multi-platform**: Only for production releases
|
||||
```yaml
|
||||
platforms: ${{ github.event_name == 'release' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
|
||||
```
|
||||
|
||||
2. **Tagging Strategy**:
|
||||
```yaml
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
```
|
||||
|
||||
### Deployment Strategy
|
||||
|
||||
1. **Staging**: Automatic deployment from main branch
|
||||
2. **Production**: Manual approval required, only from tags
|
||||
3. **Rollback**: Automated rollback on health check failure
|
||||
|
||||
### Job Dependencies
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
build:
|
||||
needs: test
|
||||
if: success() # Explicit success check
|
||||
|
||||
deploy:
|
||||
needs: [test, build]
|
||||
if: success() && github.ref == 'refs/heads/main'
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Conditional Docker Builds
|
||||
|
||||
```yaml
|
||||
# Only build when Docker files or source code changes
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docker: ${{ steps.filter.outputs.docker }}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
docker:
|
||||
- 'Dockerfile*'
|
||||
- 'src/**'
|
||||
- 'package*.json'
|
||||
|
||||
build:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.docker == 'true'
|
||||
```
|
||||
|
||||
### Deployment with Notification
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deploy
|
||||
run: ./deploy.sh
|
||||
|
||||
- name: Notify status
|
||||
if: always()
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ steps.deploy.outcome }}
|
||||
text: |
|
||||
Deployment to ${{ github.event.deployment.environment }}
|
||||
Status: ${{ steps.deploy.outcome }}
|
||||
Version: ${{ github.ref_name }}
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
1. **No hardcoded values**: Everything should be configurable
|
||||
2. **No ignored errors**: Use proper error handling, not `|| true`
|
||||
3. **No unnecessary matrix builds**: Only test multiple versions in CI, not deploy
|
||||
4. **No secrets in logs**: Use masks and secure handling
|
||||
5. **No missing health checks**: Always verify deployments
|
||||
6. **No duplicate workflows**: Use reusable workflows for common tasks
|
||||
7. **No missing permissions**: Always specify required permissions
|
||||
|
||||
## Workflow Types (Simplified)
|
||||
|
||||
### 1. Pull Request (`pull-request.yml`)
|
||||
- Fast feedback loop
|
||||
- Lint, unit tests, basic security
|
||||
- Docker build only if relevant files changed
|
||||
|
||||
### 2. Main Pipeline (`main.yml`)
|
||||
- Complete testing and deployment
|
||||
- Coverage reporting, security scans
|
||||
- Docker builds and publishing
|
||||
|
||||
## Checklist for New Workflows
|
||||
|
||||
- [ ] Uses environment variables instead of hardcoded values
|
||||
- [ ] Specifies minimum required permissions
|
||||
- [ ] Includes proper error handling
|
||||
- [ ] Has health checks for deployments
|
||||
- [ ] Uses caching effectively
|
||||
- [ ] Follows naming conventions
|
||||
- [ ] Includes security scanning
|
||||
- [ ] Has clear documentation
|
||||
- [ ] Avoids anti-patterns
|
||||
- [ ] Tested in a feature branch first
|
||||
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -9,9 +9,9 @@ updates:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
reviewers:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
assignees:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
# Enable version updates for Docker
|
||||
@@ -23,9 +23,9 @@ updates:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
reviewers:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
assignees:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
|
||||
# Enable version updates for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
@@ -36,6 +36,6 @@ updates:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
reviewers:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
assignees:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
320
.github/workflows/ci.yml
vendored
320
.github/workflows/ci.yml
vendored
@@ -1,320 +0,0 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# Lint job - fast and independent
|
||||
lint:
|
||||
name: Lint & Format Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint:check || echo "No lint script found, skipping"
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check || echo "No format script found, skipping"
|
||||
|
||||
# Unit tests - fastest test suite
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
# Integration tests - moderate complexity
|
||||
test-integration:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration || echo "No integration tests found, skipping"
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
# E2E tests - only 1 scenario, run on GitHub for simplicity
|
||||
test-e2e:
|
||||
name: E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run e2e tests
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
# Coverage generation - depends on unit tests
|
||||
coverage:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-unit]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Generate test coverage
|
||||
run: npm run test:coverage
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
# Security scans - run on GitHub for faster execution
|
||||
security:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run npm audit
|
||||
run: npm audit --audit-level=moderate
|
||||
|
||||
- name: Run security scan with Snyk
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --severity-threshold=high
|
||||
|
||||
# Check if Docker-related files changed
|
||||
changes:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
docker:
|
||||
- 'Dockerfile*'
|
||||
- 'scripts/**'
|
||||
- '.dockerignore'
|
||||
- 'claude-config*'
|
||||
src:
|
||||
- 'src/**'
|
||||
- 'package*.json'
|
||||
|
||||
# Docker builds - only when relevant files change
|
||||
docker:
|
||||
name: Docker Build & Test
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
# Security: Only run on self-hosted for trusted sources
|
||||
if: (github.event.pull_request.head.repo.owner.login == 'intelligence-assist' || github.event_name != 'pull_request') && (needs.changes.outputs.docker == 'true' || needs.changes.outputs.src == 'true')
|
||||
# Only need unit tests to pass for Docker builds
|
||||
needs: [test-unit, lint, changes]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Start build profiling
|
||||
run: |
|
||||
echo "BUILD_START_TIME=$(date +%s)" >> $GITHUB_ENV
|
||||
echo "🏗️ Docker build started at $(date)"
|
||||
|
||||
- name: Set up Docker layer caching
|
||||
run: |
|
||||
# Create cache mount directories
|
||||
mkdir -p /tmp/.buildx-cache-main /tmp/.buildx-cache-claude
|
||||
|
||||
- name: Build main Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
tags: claude-github-webhook:test
|
||||
cache-from: |
|
||||
type=gha,scope=main
|
||||
type=local,src=/tmp/.buildx-cache-main
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=main
|
||||
type=local,dest=/tmp/.buildx-cache-main-new,mode=max
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
|
||||
- name: Build Claude Code Docker image (parallel)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.claudecode
|
||||
push: false
|
||||
load: true
|
||||
tags: claude-code-runner:test
|
||||
cache-from: |
|
||||
type=gha,scope=claudecode
|
||||
type=local,src=/tmp/.buildx-cache-claude
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=claudecode
|
||||
type=local,dest=/tmp/.buildx-cache-claude-new,mode=max
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
|
||||
- name: Rotate build caches
|
||||
run: |
|
||||
# Rotate caches to avoid size limits
|
||||
rm -rf /tmp/.buildx-cache-main /tmp/.buildx-cache-claude
|
||||
mv /tmp/.buildx-cache-main-new /tmp/.buildx-cache-main 2>/dev/null || true
|
||||
mv /tmp/.buildx-cache-claude-new /tmp/.buildx-cache-claude 2>/dev/null || true
|
||||
|
||||
- name: Profile build performance
|
||||
run: |
|
||||
BUILD_END_TIME=$(date +%s)
|
||||
BUILD_DURATION=$((BUILD_END_TIME - BUILD_START_TIME))
|
||||
echo "🏁 Docker build completed at $(date)"
|
||||
echo "⏱️ Total build time: ${BUILD_DURATION} seconds"
|
||||
|
||||
# Check image sizes
|
||||
echo "📦 Image sizes:"
|
||||
docker images | grep -E "(claude-github-webhook|claude-code-runner):test" || true
|
||||
|
||||
# Show cache usage
|
||||
echo "💾 Cache statistics:"
|
||||
du -sh /tmp/.buildx-cache-* 2>/dev/null || echo "No local caches found"
|
||||
|
||||
# Performance summary
|
||||
if [ $BUILD_DURATION -lt 120 ]; then
|
||||
echo "✅ Fast build (< 2 minutes)"
|
||||
elif [ $BUILD_DURATION -lt 300 ]; then
|
||||
echo "⚠️ Moderate build (2-5 minutes)"
|
||||
else
|
||||
echo "🐌 Slow build (> 5 minutes) - consider optimization"
|
||||
fi
|
||||
|
||||
- name: Test Docker containers
|
||||
run: |
|
||||
# Test main container starts correctly
|
||||
docker run --name test-webhook -d -p 3003:3002 \
|
||||
-e NODE_ENV=test \
|
||||
-e BOT_USERNAME=@TestBot \
|
||||
-e GITHUB_WEBHOOK_SECRET=test-secret \
|
||||
-e GITHUB_TOKEN=test-token \
|
||||
claude-github-webhook:test
|
||||
|
||||
# Wait for container to start
|
||||
sleep 10
|
||||
|
||||
# Test health endpoint
|
||||
curl -f http://localhost:3003/health || exit 1
|
||||
|
||||
# Cleanup
|
||||
docker stop test-webhook
|
||||
docker rm test-webhook
|
||||
31
.github/workflows/deploy.yml
vendored
31
.github/workflows/deploy.yml
vendored
@@ -4,11 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
tags:
|
||||
- 'v*.*.*' # Semantic versioning tags (v1.0.0, v2.1.3, etc.)
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -40,14 +37,14 @@ jobs:
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
run: npm run lint:check
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Upload coverage
|
||||
if: matrix.node-version == '20.x'
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -75,9 +72,9 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build Docker Image
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
# Security: Only run on self-hosted for trusted sources AND when files changed
|
||||
if: (github.event.pull_request.head.repo.owner.login == 'intelligence-assist' || github.event_name != 'pull_request') && (needs.changes.outputs.docker == 'true' || needs.changes.outputs.src == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
# Only build when files changed and not a pull request
|
||||
if: github.event_name != 'pull_request' && (needs.changes.outputs.docker == 'true' || needs.changes.outputs.src == 'true')
|
||||
needs: [test, changes]
|
||||
|
||||
outputs:
|
||||
@@ -114,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
@@ -152,20 +149,22 @@ jobs:
|
||||
output: 'trivy-results.sarif'
|
||||
|
||||
- name: Upload Trivy scan results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
# ============================================
|
||||
# CD Jobs - Run on self-hosted runners
|
||||
# CD Jobs - Run on GitHub-hosted runners
|
||||
# ============================================
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
needs: [build, security-scan]
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
environment: staging
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.STAGING_URL }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -217,10 +216,10 @@ jobs:
|
||||
name: Deploy to Production
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build, security-scan]
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: production
|
||||
url: https://webhook.yourdomain.com
|
||||
url: ${{ vars.PRODUCTION_URL }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -287,7 +286,7 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
deployment_id: deployment.data.id,
|
||||
state: 'success',
|
||||
environment_url: 'https://webhook.yourdomain.com',
|
||||
environment_url: '${{ vars.PRODUCTION_URL }}',
|
||||
description: `Deployed version ${context.ref.replace('refs/tags/', '')}`
|
||||
});
|
||||
|
||||
|
||||
124
.github/workflows/docker-publish.yml
vendored
124
.github/workflows/docker-publish.yml
vendored
@@ -7,42 +7,43 @@ on:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Dockerfile*'
|
||||
- 'package*.json'
|
||||
- '.github/workflows/docker-publish.yml'
|
||||
- 'src/**'
|
||||
- 'scripts/**'
|
||||
- 'claude-config*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'Dockerfile*'
|
||||
- 'package*.json'
|
||||
- '.github/workflows/docker-publish.yml'
|
||||
- 'src/**'
|
||||
- 'scripts/**'
|
||||
- 'claude-config*'
|
||||
|
||||
env:
|
||||
DOCKER_HUB_USERNAME: cheffromspace
|
||||
DOCKER_HUB_ORGANIZATION: intelligenceassist
|
||||
IMAGE_NAME: claude-github-webhook
|
||||
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'cheffromspace' }}
|
||||
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION || 'intelligenceassist' }}
|
||||
IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'claude-hub' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
# Security: Only run on self-hosted for trusted sources
|
||||
if: github.event.pull_request.head.repo.owner.login == 'intelligence-assist' || github.event_name != 'pull_request'
|
||||
# Always use GitHub-hosted runners
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Runner Information
|
||||
run: |
|
||||
echo "Running on: ${{ runner.name }}"
|
||||
echo "Runner OS: ${{ runner.os }}"
|
||||
echo "Runner labels: ${{ join(runner.labels, ', ') }}"
|
||||
|
||||
- name: Clean workspace (fix coverage permissions)
|
||||
run: |
|
||||
# Fix any existing coverage file permissions before checkout
|
||||
sudo find . -name "coverage" -type d -exec chmod -R 755 {} \; 2>/dev/null || true
|
||||
sudo rm -rf coverage 2>/dev/null || true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -60,37 +61,52 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
# For branches (master/main), use 'staging' tag
|
||||
type=ref,event=branch,suffix=-staging
|
||||
# For semantic version tags, use the version
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
# Latest tag for semantic version tags
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
# SHA for branch builds (push only)
|
||||
type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'pull_request' }}
|
||||
# For PR builds, use pr-NUMBER
|
||||
type=ref,event=pr
|
||||
type=raw,value=nightly,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
# Build and test in container for PRs
|
||||
- name: Build and test Docker image (PR)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
# Build the test stage
|
||||
docker build --target test -t ${{ env.IMAGE_NAME }}:test-${{ github.sha }} -f Dockerfile .
|
||||
|
||||
# Run tests in container
|
||||
docker run --rm \
|
||||
-e CI=true \
|
||||
-e NODE_ENV=test \
|
||||
-v ${{ github.workspace }}/coverage:/app/coverage \
|
||||
${{ env.IMAGE_NAME }}:test-${{ github.sha }} \
|
||||
npm test
|
||||
|
||||
# Build production image for smoke test
|
||||
docker build --target production -t ${{ env.IMAGE_NAME }}:pr-${{ github.event.number }} -f Dockerfile .
|
||||
|
||||
# Smoke test
|
||||
docker run --rm ${{ env.IMAGE_NAME }}:pr-${{ github.event.number }} \
|
||||
test -f /app/scripts/runtime/startup.sh && echo "✓ Startup script exists"
|
||||
|
||||
# Build and push for main branch
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: |
|
||||
type=gha,scope=publish-main
|
||||
type=local,src=/tmp/.buildx-cache-main
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=publish-main
|
||||
type=local,dest=/tmp/.buildx-cache-main-new,mode=max
|
||||
target: production
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Update Docker Hub Description
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: peter-evans/dockerhub-description@v3
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ env.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
@@ -98,18 +114,26 @@ jobs:
|
||||
readme-filepath: ./README.dockerhub.md
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
|
||||
# Additional job to build and push the Claude Code container
|
||||
# Build claudecode separately
|
||||
build-claudecode:
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
# Security: Only run on self-hosted for trusted sources + not on PRs
|
||||
if: (github.event.pull_request.head.repo.owner.login == 'intelligence-assist' || github.event_name != 'pull_request') && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Clean workspace (fix coverage permissions)
|
||||
run: |
|
||||
# Fix any existing coverage file permissions before checkout
|
||||
sudo find . -name "coverage" -type d -exec chmod -R 755 {} \; 2>/dev/null || true
|
||||
sudo rm -rf coverage 2>/dev/null || true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -126,12 +150,14 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.DOCKER_HUB_ORGANIZATION }}/claudecode
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=-staging
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=raw,value=nightly,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push Claude Code Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.claudecode
|
||||
@@ -139,9 +165,7 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta-claudecode.outputs.tags }}
|
||||
labels: ${{ steps.meta-claudecode.outputs.labels }}
|
||||
cache-from: |
|
||||
type=gha,scope=publish-claudecode
|
||||
type=local,src=/tmp/.buildx-cache-claude
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=publish-claudecode
|
||||
type=local,dest=/tmp/.buildx-cache-claude-new,mode=max
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Note: Fallback job removed since we're always using GitHub-hosted runners
|
||||
66
.github/workflows/main.yml
vendored
Normal file
66
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Main Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint:check
|
||||
- run: npm run test:ci
|
||||
env:
|
||||
NODE_ENV: test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/security/credential-audit.sh
|
||||
- uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
||||
head: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
extra_args: --debug --only-verified
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, security]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
40
.github/workflows/pull-request.yml
vendored
Normal file
40
.github/workflows/pull-request.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint:check
|
||||
- run: npm run test:unit
|
||||
env:
|
||||
NODE_ENV: test
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/security/credential-audit.sh
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.pull_request.changed_files, 'Dockerfile') || contains(github.event.pull_request.changed_files, 'src/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: test:latest
|
||||
41
.github/workflows/security-audit.yml
vendored
41
.github/workflows/security-audit.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Security Audit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
schedule:
|
||||
# Run daily at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
security-audit:
|
||||
runs-on: ubuntu-latest
|
||||
name: Security Audit
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch full history for comprehensive scanning
|
||||
|
||||
- name: Run credential audit
|
||||
run: ./scripts/security/credential-audit.sh
|
||||
|
||||
|
||||
- name: Check for high-risk files
|
||||
run: |
|
||||
# Check for files that commonly contain secrets
|
||||
risk_files=$(find . -name "*.pem" -o -name "*.key" -o -name "*.p12" -o -name "*.pfx" -o -name "*secret*" -o -name "*password*" -o -name "*credential*" | grep -v node_modules || true)
|
||||
if [ ! -z "$risk_files" ]; then
|
||||
echo "⚠️ Found high-risk files that may contain secrets:"
|
||||
echo "$risk_files"
|
||||
echo "::warning::High-risk files detected. Please review for secrets."
|
||||
fi
|
||||
|
||||
- name: Audit npm packages
|
||||
run: |
|
||||
if [ -f "package.json" ]; then
|
||||
npm audit --audit-level=high
|
||||
fi
|
||||
140
.github/workflows/security.yml
vendored
140
.github/workflows/security.yml
vendored
@@ -1,17 +1,20 @@
|
||||
name: Security Scans
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run security scans daily at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
# Run daily at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
dependency-scan:
|
||||
name: Dependency Security Scan
|
||||
dependency-audit:
|
||||
name: Dependency Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -29,57 +32,79 @@ jobs:
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run npm audit
|
||||
run: npm audit --audit-level=moderate
|
||||
run: |
|
||||
npm audit --audit-level=moderate || {
|
||||
echo "::warning::npm audit found vulnerabilities"
|
||||
exit 0 # Don't fail the build, but warn
|
||||
}
|
||||
|
||||
- name: Check for known vulnerabilities
|
||||
run: npm run security:audit
|
||||
run: npm run security:audit || echo "::warning::Security audit script failed"
|
||||
|
||||
secret-scan:
|
||||
name: Secret Scanning
|
||||
secret-scanning:
|
||||
name: Secret and Credential Scanning
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 0 # Full history for secret scanning
|
||||
|
||||
- name: TruffleHog OSS
|
||||
- name: Run credential audit script
|
||||
run: |
|
||||
if [ -f "./scripts/security/credential-audit.sh" ]; then
|
||||
./scripts/security/credential-audit.sh || {
|
||||
echo "::error::Credential audit failed"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "::warning::Credential audit script not found"
|
||||
fi
|
||||
|
||||
- name: TruffleHog Secret Scan
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}
|
||||
head: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
|
||||
base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
||||
head: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
extra_args: --debug --only-verified
|
||||
|
||||
codeql:
|
||||
name: CodeQL Analysis
|
||||
- name: Check for high-risk files
|
||||
run: |
|
||||
# Check for files that commonly contain secrets
|
||||
risk_files=$(find . -type f \( \
|
||||
-name "*.pem" -o \
|
||||
-name "*.key" -o \
|
||||
-name "*.p12" -o \
|
||||
-name "*.pfx" -o \
|
||||
-name "*secret*" -o \
|
||||
-name "*password*" -o \
|
||||
-name "*credential*" \
|
||||
\) -not -path "*/node_modules/*" -not -path "*/.git/*" | head -20)
|
||||
|
||||
if [ -n "$risk_files" ]; then
|
||||
echo "⚠️ Found potentially sensitive files:"
|
||||
echo "$risk_files"
|
||||
echo "::warning::High-risk files detected. Please ensure they don't contain secrets."
|
||||
fi
|
||||
|
||||
codeql-analysis:
|
||||
name: CodeQL Security Analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
languages: javascript
|
||||
config-file: ./.github/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
@@ -88,4 +113,57 @@ jobs:
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
category: "/language:javascript"
|
||||
|
||||
docker-security:
|
||||
name: Docker Image Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on main branch pushes or when Docker files change
|
||||
if: github.ref == 'refs/heads/main' || contains(github.event.head_commit.modified, 'Dockerfile')
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Hadolint
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
failure-threshold: warning
|
||||
|
||||
- name: Build test image for scanning
|
||||
run: docker build -t test-image:${{ github.sha }} .
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: test-image:${{ github.sha }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
- name: Upload Trivy scan results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
security-summary:
|
||||
name: Security Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dependency-audit, secret-scanning, codeql-analysis, docker-security]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Check job statuses
|
||||
run: |
|
||||
echo "## Security Scan Summary"
|
||||
echo "- Dependency Audit: ${{ needs.dependency-audit.result }}"
|
||||
echo "- Secret Scanning: ${{ needs.secret-scanning.result }}"
|
||||
echo "- CodeQL Analysis: ${{ needs.codeql-analysis.result }}"
|
||||
echo "- Docker Security: ${{ needs.docker-security.result }}"
|
||||
|
||||
if [[ "${{ needs.secret-scanning.result }}" == "failure" ]]; then
|
||||
echo "::error::Secret scanning failed - potential credentials detected!"
|
||||
exit 1
|
||||
fi
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules/
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.template
|
||||
!.env.quickstart
|
||||
|
||||
# Logs
|
||||
logs
|
||||
@@ -22,6 +23,19 @@ pids
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
test-results/
|
||||
|
||||
# TypeScript build artifacts
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# TypeScript compiled test files
|
||||
test/**/*.d.ts
|
||||
test/**/*.d.ts.map
|
||||
test/**/*.js.map
|
||||
# Don't ignore the actual test files
|
||||
!test/**/*.test.js
|
||||
!test/**/*.spec.js
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
@@ -64,11 +78,14 @@ config
|
||||
auth.json
|
||||
service-account.json
|
||||
|
||||
# Claude authentication output
|
||||
.claude-hub/
|
||||
claude-config/
|
||||
claude-config*
|
||||
|
||||
# Docker secrets
|
||||
secrets/
|
||||
|
||||
# Benchmark results
|
||||
benchmark_results_*.json
|
||||
|
||||
# Temporary and backup files
|
||||
*.backup
|
||||
@@ -79,4 +96,4 @@ benchmark_results_*.json
|
||||
# Root level clutter prevention
|
||||
/test-*.js
|
||||
/PR_SUMMARY.md
|
||||
/*-proposal.md
|
||||
/*-proposal.md
|
||||
|
||||
25
.husky/pre-commit
Executable file
25
.husky/pre-commit
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "🎨 Running Prettier check..."
|
||||
if ! npm run format:check; then
|
||||
echo "❌ Prettier formatting issues found!"
|
||||
echo "💡 Run 'npm run format' to fix formatting issues, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔍 Running ESLint check..."
|
||||
if ! npm run lint:check; then
|
||||
echo "❌ ESLint issues found!"
|
||||
echo "💡 Run 'npm run lint' to fix linting issues, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📝 Running TypeScript check..."
|
||||
if ! npm run typecheck; then
|
||||
echo "❌ TypeScript errors found!"
|
||||
echo "💡 Fix TypeScript errors, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All pre-commit checks passed!"
|
||||
@@ -1,39 +1,37 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-yaml
|
||||
- id: detect-private-key
|
||||
|
||||
- repo: https://github.com/Yelp/detect-secrets
|
||||
rev: v1.4.0
|
||||
rev: v1.5.0
|
||||
hooks:
|
||||
- id: detect-secrets
|
||||
args: ['--baseline', '.secrets.baseline']
|
||||
exclude: node_modules/
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.18.1
|
||||
exclude: package-lock.json
|
||||
|
||||
- repo: https://github.com/zricethezav/gitleaks
|
||||
rev: v8.21.2
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
- repo: https://github.com/thoughtworks/talisman
|
||||
rev: v1.32.0
|
||||
hooks:
|
||||
- id: talisman-commit
|
||||
entry: cmd --githook pre-commit
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: env-file-check
|
||||
name: Check for .env files
|
||||
entry: bash -c 'if find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" | grep -q .; then echo "Found .env files that may contain secrets"; exit 1; fi'
|
||||
- id: eslint
|
||||
name: eslint
|
||||
entry: npm run lint:check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: credential-scan
|
||||
name: Scan for hardcoded credentials
|
||||
entry: bash -c 'if grep -r "sk-\|ghp_\|AKIA\|xox[boas]\|AIza[0-9A-Za-z\\-_]\{35\}" --exclude-dir=node_modules --exclude-dir=.git .; then echo "Found potential hardcoded credentials"; exit 1; fi'
|
||||
files: \.(js|ts)$
|
||||
|
||||
- id: prettier
|
||||
name: prettier
|
||||
entry: npm run format:check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: \.(js|ts|json|md)$
|
||||
154
CLAUDE.md
154
CLAUDE.md
@@ -18,18 +18,25 @@ This repository contains a webhook service that integrates Claude with GitHub, a
|
||||
|
||||
## Build & Run Commands
|
||||
|
||||
### TypeScript Build Commands
|
||||
- **Build TypeScript**: `npm run build` (compiles to `dist/` directory)
|
||||
- **Build TypeScript (watch mode)**: `npm run build:watch`
|
||||
- **Type checking only**: `npm run typecheck` (no compilation)
|
||||
- **Clean build artifacts**: `npm run clean`
|
||||
|
||||
### Setup and Installation
|
||||
- **Initial setup**: `./scripts/setup.sh`
|
||||
- **Setup secure credentials**: `./scripts/setup/setup-secure-credentials.sh`
|
||||
- **Start with Docker (recommended)**: `docker compose up -d`
|
||||
- **Start the server locally**: `npm start`
|
||||
- **Development mode with auto-restart**: `npm run dev`
|
||||
- **Start production build**: `npm start` (runs compiled JavaScript from `dist/`)
|
||||
- **Start development build**: `npm run start:dev` (runs JavaScript directly from `src/`)
|
||||
- **Development mode with TypeScript**: `npm run dev` (uses ts-node)
|
||||
- **Development mode with auto-restart**: `npm run dev:watch` (uses nodemon + ts-node)
|
||||
- **Start on specific port**: `./scripts/runtime/start-api.sh` (uses port 3003)
|
||||
- **Run tests**: `npm test`
|
||||
- Run specific test types:
|
||||
- Unit tests: `npm run test:unit`
|
||||
- Integration tests: `npm run test:integration`
|
||||
- End-to-end tests: `npm run test:e2e`
|
||||
- Unit tests: `npm run test:unit` (supports both `.js` and `.ts` files)
|
||||
- End-to-end tests: `npm run test:e2e` (supports both `.js` and `.ts` files)
|
||||
- Test with coverage: `npm run test:coverage`
|
||||
- Watch mode: `npm run test:watch`
|
||||
|
||||
@@ -39,7 +46,7 @@ This repository contains a webhook service that integrates Claude with GitHub, a
|
||||
- **View logs**: `docker compose logs -f webhook`
|
||||
- **Restart**: `docker compose restart webhook`
|
||||
- Build Claude container: `./build-claude-container.sh`
|
||||
- Build Claude Code container: `./scripts/build/build-claudecode.sh`
|
||||
- Build Claude Code container: `./scripts/build/build.sh claudecode`
|
||||
- Update production image: `./update-production-image.sh`
|
||||
|
||||
### AWS Credential Management
|
||||
@@ -64,10 +71,18 @@ This repository contains a webhook service that integrates Claude with GitHub, a
|
||||
- Fix security vulnerabilities: `npm run security:fix`
|
||||
- All CI tests: `npm run test:ci` (includes coverage)
|
||||
|
||||
### Pre-commit Hooks
|
||||
The project uses Husky for Git pre-commit hooks to ensure code quality:
|
||||
- **ESLint**: Checks code for linting errors
|
||||
- **Prettier**: Validates code formatting
|
||||
- **TypeScript**: Runs type checking
|
||||
- **Setup**: Hooks are automatically installed via `npm run prepare`
|
||||
- **Manual run**: Execute `.husky/pre-commit` to test locally
|
||||
|
||||
### End-to-End Testing
|
||||
Use the demo repository for testing auto-tagging and webhook functionality:
|
||||
- Demo repository: `https://github.com/intelligence-assist/demo-repository`
|
||||
- Test auto-tagging: `./cli/webhook-cli.js --repo "intelligence-assist/demo-repository" --command "Auto-tag this issue" --issue 1 --url "http://localhost:8082"`
|
||||
- Demo repository: `https://github.com/claude-did-this/demo-repository`
|
||||
- Test auto-tagging: `./cli/webhook-cli.js --repo "claude-did-this/demo-repository" --command "Auto-tag this issue" --issue 1 --url "http://localhost:8082"`
|
||||
- Test with specific issue content: Create a new issue in the demo repository to trigger auto-tagging webhook
|
||||
- Verify labels are applied based on issue content analysis
|
||||
|
||||
@@ -82,16 +97,57 @@ Use the demo repository for testing auto-tagging and webhook functionality:
|
||||
- Advanced usage: `node cli/webhook-cli.js --repo myrepo --command "Your command" --verbose`
|
||||
- Secure mode: `node cli/webhook-cli-secure.js` (uses AWS profile authentication)
|
||||
|
||||
### Claude Authentication Options
|
||||
|
||||
This service supports three authentication methods:
|
||||
|
||||
- **Setup Container**: Personal subscription authentication - [Setup Container Guide](./docs/setup-container-guide.md)
|
||||
- **ANTHROPIC_API_KEY**: Direct API key authentication - [Authentication Guide](./docs/claude-authentication-guide.md)
|
||||
- **AWS Bedrock**: Enterprise AWS integration - [Authentication Guide](./docs/claude-authentication-guide.md)
|
||||
|
||||
#### Quick Start: Setup Container
|
||||
For personal subscription users:
|
||||
|
||||
```bash
|
||||
# 1. Run interactive authentication setup
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
# 2. In container: authenticate with your subscription
|
||||
claude --dangerously-skip-permissions # Follow authentication flow
|
||||
exit # Save authentication
|
||||
|
||||
# 3. Test captured authentication
|
||||
./scripts/setup/test-claude-auth.sh
|
||||
|
||||
# 4. Use in production
|
||||
cp -r ${CLAUDE_HUB_DIR:-~/.claude-hub}/* ~/.claude/
|
||||
```
|
||||
|
||||
📖 **See [Complete Authentication Guide](./docs/claude-authentication-guide.md) for all methods**
|
||||
|
||||
## Features
|
||||
|
||||
### Auto-Tagging
|
||||
The system automatically analyzes new issues and applies appropriate labels based on:
|
||||
The system automatically analyzes new issues and applies appropriate labels using a secure, minimal-permission approach:
|
||||
|
||||
**Security Features:**
|
||||
- **Minimal Tool Access**: Uses only `Read` and `GitHub` tools (no file editing or bash execution)
|
||||
- **Dedicated Container**: Runs in specialized container with restricted entrypoint script
|
||||
- **CLI-Based**: Uses `gh` CLI commands directly instead of JSON parsing for better reliability
|
||||
|
||||
**Label Categories:**
|
||||
- **Priority**: critical, high, medium, low
|
||||
- **Type**: bug, feature, enhancement, documentation, question, security
|
||||
- **Complexity**: trivial, simple, moderate, complex
|
||||
- **Component**: api, frontend, backend, database, auth, webhook, docker
|
||||
|
||||
When an issue is opened, Claude analyzes the title and description to suggest intelligent labels, with keyword-based fallback for reliability.
|
||||
**Process Flow:**
|
||||
1. New issue triggers `issues.opened` webhook
|
||||
2. Dedicated Claude container starts with `claudecode-tagging-entrypoint.sh`
|
||||
3. Claude analyzes issue content using minimal tools
|
||||
4. Labels applied directly via `gh issue edit --add-label` commands
|
||||
5. No comments posted (silent operation)
|
||||
6. Fallback to keyword-based labeling if CLI approach fails
|
||||
|
||||
### Automated PR Review
|
||||
The system automatically triggers comprehensive PR reviews when all checks pass:
|
||||
@@ -104,35 +160,47 @@ The system automatically triggers comprehensive PR reviews when all checks pass:
|
||||
## Architecture Overview
|
||||
|
||||
### Core Components
|
||||
1. **Express Server** (`src/index.js`): Main application entry point that sets up middleware, routes, and error handling
|
||||
1. **Express Server** (`src/index.ts`): Main application entry point that sets up middleware, routes, and error handling
|
||||
2. **Routes**:
|
||||
- GitHub Webhook: `/api/webhooks/github` - Processes GitHub webhook events
|
||||
- Claude API: `/api/claude` - Direct API access to Claude
|
||||
- Health Check: `/health` - Service status monitoring
|
||||
3. **Controllers**:
|
||||
- `githubController.js` - Handles webhook verification and processing
|
||||
- `githubController.ts` - Handles webhook verification and processing
|
||||
4. **Services**:
|
||||
- `claudeService.js` - Interfaces with Claude Code CLI
|
||||
- `githubService.js` - Handles GitHub API interactions
|
||||
- `claudeService.ts` - Interfaces with Claude Code CLI
|
||||
- `githubService.ts` - Handles GitHub API interactions
|
||||
5. **Utilities**:
|
||||
- `logger.js` - Logging functionality with redaction capability
|
||||
- `awsCredentialProvider.js` - Secure AWS credential management
|
||||
- `sanitize.js` - Input sanitization and security
|
||||
- `logger.ts` - Logging functionality with redaction capability
|
||||
- `awsCredentialProvider.ts` - Secure AWS credential management
|
||||
- `sanitize.ts` - Input sanitization and security
|
||||
|
||||
### Execution Modes
|
||||
- **Direct mode**: Runs Claude Code CLI locally
|
||||
- **Container mode**: Runs Claude in isolated Docker containers with elevated privileges
|
||||
### Execution Modes & Security Architecture
|
||||
The system uses different execution modes based on operation type:
|
||||
|
||||
### DevContainer Configuration
|
||||
The repository includes a `.devcontainer` configuration that allows Claude Code to run with:
|
||||
**Operation Types:**
|
||||
- **Auto-tagging**: Minimal permissions (`Read`, `GitHub` tools only)
|
||||
- **PR Review**: Standard permissions (full tool set)
|
||||
- **Default**: Standard permissions (full tool set)
|
||||
|
||||
**Security Features:**
|
||||
- **Tool Allowlists**: Each operation type uses specific tool restrictions
|
||||
- **Dedicated Entrypoints**: Separate container entrypoint scripts for different operations
|
||||
- **No Dangerous Permissions**: System avoids `--dangerously-skip-permissions` flag
|
||||
- **Container Isolation**: Docker containers with minimal required capabilities
|
||||
|
||||
**Container Entrypoints:**
|
||||
- `claudecode-tagging-entrypoint.sh`: Minimal tools for auto-tagging (`--allowedTools Read,GitHub`)
|
||||
- `claudecode-entrypoint.sh`: Full tools for general operations (`--allowedTools Bash,Create,Edit,Read,Write,GitHub`)
|
||||
|
||||
**DevContainer Configuration:**
|
||||
The repository includes a `.devcontainer` configuration for development:
|
||||
- Privileged mode for system-level access
|
||||
- Network capabilities (NET_ADMIN, NET_RAW) for firewall management
|
||||
- System capabilities (SYS_TIME, DAC_OVERRIDE, AUDIT_WRITE, SYS_ADMIN)
|
||||
- Docker socket mounting for container management
|
||||
- Automatic firewall initialization via post-create command
|
||||
|
||||
This configuration enables the use of `--dangerously-skip-permissions` flag when running Claude Code CLI.
|
||||
|
||||
### Workflow
|
||||
1. GitHub comment with bot mention (configured via BOT_USERNAME) triggers a webhook event
|
||||
2. Express server receives the webhook at `/api/webhooks/github`
|
||||
@@ -147,7 +215,7 @@ The service supports multiple AWS authentication methods, with a focus on securi
|
||||
- **Task Roles** (ECS): Automatically uses container credentials
|
||||
- **Direct credentials**: Not recommended, but supported for backward compatibility
|
||||
|
||||
The `awsCredentialProvider.js` utility handles credential retrieval and rotation.
|
||||
The `awsCredentialProvider.ts` utility handles credential retrieval and rotation.
|
||||
|
||||
## Security Features
|
||||
- Webhook signature verification using HMAC
|
||||
@@ -174,9 +242,41 @@ The `awsCredentialProvider.js` utility handles credential retrieval and rotation
|
||||
- `GITHUB_TOKEN`: GitHub token for API access
|
||||
- `ANTHROPIC_API_KEY`: Anthropic API key for Claude access
|
||||
|
||||
### Optional Environment Variables
|
||||
- `PR_REVIEW_WAIT_FOR_ALL_CHECKS`: Set to `"true"` to wait for all meaningful check suites to complete successfully before triggering PR review (default: `"true"`). Uses smart logic to handle conditional jobs and skipped checks, preventing duplicate reviews from different check suites.
|
||||
- `PR_REVIEW_TRIGGER_WORKFLOW`: Name of a specific GitHub Actions workflow that should trigger PR reviews (e.g., `"Pull Request CI"`). Only used if `PR_REVIEW_WAIT_FOR_ALL_CHECKS` is `"false"`.
|
||||
- `PR_REVIEW_DEBOUNCE_MS`: Delay in milliseconds before checking all check suites status (default: `"5000"`). This accounts for GitHub's eventual consistency.
|
||||
- `PR_REVIEW_MAX_WAIT_MS`: Maximum time to wait for stale in-progress check suites before considering them failed (default: `"1800000"` = 30 minutes).
|
||||
- `PR_REVIEW_CONDITIONAL_TIMEOUT_MS`: Time to wait for conditional jobs that never start before skipping them (default: `"300000"` = 5 minutes).
|
||||
|
||||
## TypeScript Infrastructure
|
||||
The project is configured with TypeScript for enhanced type safety and developer experience:
|
||||
|
||||
### Configuration Files
|
||||
- **tsconfig.json**: TypeScript compiler configuration with strict mode enabled
|
||||
- **eslint.config.js**: ESLint configuration with TypeScript support and strict rules
|
||||
- **jest.config.js**: Jest configuration with ts-jest for TypeScript test support
|
||||
- **babel.config.js**: Babel configuration for JavaScript file transformation
|
||||
|
||||
### Build Process
|
||||
- TypeScript source files in `src/` compile to JavaScript in `dist/`
|
||||
- Support for both `.js` and `.ts` files during the transition period
|
||||
- Source maps enabled for debugging compiled code
|
||||
- Watch mode available for development with automatic recompilation
|
||||
|
||||
### Migration Strategy
|
||||
- **Phase 1** (Current): Infrastructure setup with TypeScript tooling
|
||||
- **Phase 2** (Future): Gradual conversion of JavaScript files to TypeScript
|
||||
- **Backward Compatibility**: Existing JavaScript files continue to work during transition
|
||||
|
||||
## Code Style Guidelines
|
||||
- JavaScript with Node.js
|
||||
- **TypeScript/JavaScript** with Node.js (ES2022 target)
|
||||
- Use async/await for asynchronous operations
|
||||
- Comprehensive error handling and logging
|
||||
- camelCase variable and function naming
|
||||
- Input validation and sanitization for security
|
||||
- Input validation and sanitization for security
|
||||
- **TypeScript specific**:
|
||||
- Strict mode enabled for all TypeScript files
|
||||
- Interface definitions preferred over type aliases
|
||||
- Type imports when importing only for types
|
||||
- No explicit `any` types (use `unknown` or proper typing)
|
||||
169
Dockerfile
169
Dockerfile
@@ -1,64 +1,143 @@
|
||||
FROM node:24-slim
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Install git, Claude Code, Docker, and required dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
expect \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker CLI (not the daemon, just the client)
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Claude Code
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Create docker group first, then create a non-root user for running the application
|
||||
RUN groupadd -g 999 docker || true \
|
||||
&& useradd -m -u 1001 -s /bin/bash claudeuser \
|
||||
&& usermod -aG docker claudeuser || true
|
||||
|
||||
# Create claude config directory and copy config
|
||||
RUN mkdir -p /home/claudeuser/.config/claude
|
||||
COPY claude-config.json /home/claudeuser/.config/claude/config.json
|
||||
RUN chown -R claudeuser:claudeuser /home/claudeuser/.config
|
||||
# Build stage - compile TypeScript and prepare production files
|
||||
FROM node:24-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
# Copy package files first for better caching
|
||||
COPY package*.json tsconfig.json babel.config.js ./
|
||||
|
||||
# Copy application code
|
||||
# Install all dependencies (including dev)
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY src/ ./src/
|
||||
|
||||
# Build TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# Copy remaining application files
|
||||
COPY . .
|
||||
|
||||
# Make startup script executable
|
||||
RUN chmod +x /app/scripts/runtime/startup.sh
|
||||
# Production dependency stage - smaller layer for dependencies
|
||||
FROM node:24-slim AS prod-deps
|
||||
|
||||
# Note: Docker socket will be mounted at runtime, no need to create it here
|
||||
WORKDIR /app
|
||||
|
||||
# Change ownership of the app directory to the non-root user
|
||||
RUN chown -R claudeuser:claudeuser /app
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install only production dependencies
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# Test stage - includes dev dependencies and test files
|
||||
FROM node:24-slim AS test
|
||||
|
||||
# Set shell with pipefail option
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install all dependencies
|
||||
COPY package*.json tsconfig*.json babel.config.js jest.config.js ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source and test files
|
||||
COPY src/ ./src/
|
||||
COPY test/ ./test/
|
||||
COPY scripts/ ./scripts/
|
||||
|
||||
# Copy built files from builder
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Set test environment
|
||||
ENV NODE_ENV=test
|
||||
|
||||
# Run tests by default in this stage
|
||||
CMD ["npm", "test"]
|
||||
|
||||
# Production stage - minimal runtime image
|
||||
FROM node:24-slim AS production
|
||||
|
||||
# Set shell with pipefail option for better error handling
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Install runtime dependencies with pinned versions
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git=1:2.39.5-0+deb12u2 \
|
||||
curl=7.88.1-10+deb12u12 \
|
||||
python3=3.11.2-1+b1 \
|
||||
python3-pip=23.0.1+dfsg-1 \
|
||||
python3-venv=3.11.2-1+b1 \
|
||||
expect=5.45.4-2+b1 \
|
||||
ca-certificates=20230311 \
|
||||
gnupg=2.2.40-1.1 \
|
||||
lsb-release=12.0-1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker CLI (not the daemon, just the client) with consolidated RUN and pinned versions
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends docker-ce-cli=5:27.* \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create docker group first, then create a non-root user for running the application
|
||||
RUN groupadd -g 999 docker 2>/dev/null || true \
|
||||
&& useradd -m -u 1001 -s /bin/bash claudeuser \
|
||||
&& usermod -aG docker claudeuser 2>/dev/null || true
|
||||
|
||||
# Create necessary directories and set permissions while still root
|
||||
RUN mkdir -p /home/claudeuser/.npm-global \
|
||||
&& mkdir -p /home/claudeuser/.config/claude \
|
||||
&& chown -R claudeuser:claudeuser /home/claudeuser/.npm-global /home/claudeuser/.config
|
||||
|
||||
# Configure npm to use the user directory for global packages
|
||||
ENV NPM_CONFIG_PREFIX=/home/claudeuser/.npm-global
|
||||
ENV PATH=/home/claudeuser/.npm-global/bin:$PATH
|
||||
|
||||
# Switch to non-root user and install Claude Code
|
||||
USER claudeuser
|
||||
|
||||
# Install Claude Code (latest version) as non-root user
|
||||
# hadolint ignore=DL3016
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Switch back to root for remaining setup
|
||||
USER root
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy production dependencies from prod-deps stage
|
||||
COPY --from=prod-deps /app/node_modules ./node_modules
|
||||
|
||||
# Copy built application from builder stage
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Copy configuration and runtime files
|
||||
COPY package*.json tsconfig.json babel.config.js ./
|
||||
COPY claude-config.json /home/claudeuser/.config/claude/config.json
|
||||
COPY scripts/ ./scripts/
|
||||
COPY docs/ ./docs/
|
||||
COPY cli/ ./cli/
|
||||
|
||||
# Set permissions
|
||||
RUN chown -R claudeuser:claudeuser /home/claudeuser/.config /app \
|
||||
&& chmod +x /app/scripts/runtime/startup.sh
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 3002
|
||||
|
||||
# Set default environment variables
|
||||
ENV NODE_ENV=production \
|
||||
PORT=3002
|
||||
PORT=3002 \
|
||||
NPM_CONFIG_PREFIX=/home/claudeuser/.npm-global \
|
||||
PATH=/home/claudeuser/.npm-global/bin:$PATH
|
||||
|
||||
# Stay as root user to run Docker commands
|
||||
# (The container will need to run with Docker socket mounted)
|
||||
# Switch to non-root user for running the application
|
||||
# Docker commands will work via docker group membership when socket is mounted
|
||||
USER claudeuser
|
||||
|
||||
# Run the startup script
|
||||
CMD ["bash", "/app/scripts/runtime/startup.sh"]
|
||||
108
Dockerfile.claude-setup
Normal file
108
Dockerfile.claude-setup
Normal file
@@ -0,0 +1,108 @@
|
||||
FROM node:24
|
||||
|
||||
# Install dependencies for interactive session
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
sudo \
|
||||
zsh \
|
||||
curl \
|
||||
vim \
|
||||
nano \
|
||||
gh \
|
||||
rsync
|
||||
|
||||
# Set up npm global directory
|
||||
RUN mkdir -p /usr/local/share/npm-global && \
|
||||
chown -R node:node /usr/local/share
|
||||
|
||||
# Switch to node user for npm install
|
||||
USER node
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
|
||||
# Install Claude Code
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Switch back to root for setup
|
||||
USER root
|
||||
|
||||
# Create authentication workspace
|
||||
RUN mkdir -p /auth-setup && chown -R node:node /auth-setup
|
||||
|
||||
# Set up interactive shell environment
|
||||
ENV SHELL /bin/zsh
|
||||
WORKDIR /auth-setup
|
||||
|
||||
# Create setup script
|
||||
COPY <<'EOF' /setup-claude-auth.sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔧 Claude Authentication Setup"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
echo "This will help you connect Claude to your account."
|
||||
echo ""
|
||||
echo "Quick setup - just run this command:"
|
||||
echo ""
|
||||
echo " claude --dangerously-skip-permissions && exit"
|
||||
echo ""
|
||||
echo "This will authenticate Claude and save your setup automatically."
|
||||
echo ""
|
||||
|
||||
# Function to copy authentication state
|
||||
copy_auth_state() {
|
||||
if [ -d "/home/node/.claude" ] && [ -d "/auth-output" ]; then
|
||||
echo "💾 Saving your authentication..."
|
||||
# Copy authentication files, excluding todos
|
||||
rsync -a --exclude='todos/' /home/node/.claude/ /auth-output/ 2>/dev/null || \
|
||||
cp -r /home/node/.claude/. /auth-output/ 2>/dev/null || true
|
||||
echo "✅ Authentication saved successfully!"
|
||||
fi
|
||||
}
|
||||
|
||||
# Set up signal handling to capture state on exit
|
||||
trap copy_auth_state EXIT
|
||||
|
||||
# Create .claude directory for node user
|
||||
sudo -u node mkdir -p /home/node/.claude
|
||||
|
||||
echo "🔐 Starting interactive shell as 'node' user..."
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
# Check if we should run automatically
|
||||
if [ "$1" = "--auto" ]; then
|
||||
echo "Running authentication automatically..."
|
||||
echo ""
|
||||
sudo -u node bash -c '
|
||||
export HOME=/home/node
|
||||
export PATH=/usr/local/share/npm-global/bin:$PATH
|
||||
cd /home/node
|
||||
claude --dangerously-skip-permissions
|
||||
exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Authentication command failed with exit code $exit_code"
|
||||
exit $exit_code
|
||||
fi
|
||||
'
|
||||
else
|
||||
# Switch to node user and start interactive shell
|
||||
sudo -u node bash -c '
|
||||
export HOME=/home/node
|
||||
export PATH=/usr/local/share/npm-global/bin:$PATH
|
||||
cd /home/node
|
||||
echo "Ready! Run this command to authenticate and exit:"
|
||||
echo ""
|
||||
echo " claude --dangerously-skip-permissions && exit"
|
||||
echo ""
|
||||
exec bash -i
|
||||
'
|
||||
fi
|
||||
EOF
|
||||
|
||||
RUN chmod +x /setup-claude-auth.sh
|
||||
|
||||
# Set entrypoint to setup script
|
||||
ENTRYPOINT ["/bin/bash", "/setup-claude-auth.sh"]
|
||||
@@ -44,10 +44,11 @@ RUN npm install -g @anthropic-ai/claude-code
|
||||
# Switch back to root
|
||||
USER root
|
||||
|
||||
# Copy the pre-authenticated Claude config to BOTH root and node user
|
||||
COPY claude-config /root/.claude
|
||||
COPY claude-config /home/node/.claude
|
||||
RUN chown -R node:node /home/node/.claude
|
||||
# Copy the pre-authenticated Claude config to BOTH root and node user (only for production builds)
|
||||
# For regular builds, this will be empty directories that Claude can authenticate into
|
||||
# COPY claude-config /root/.claude
|
||||
# COPY claude-config /home/node/.claude
|
||||
# RUN chown -R node:node /home/node/.claude
|
||||
|
||||
# Copy the rest of the setup
|
||||
WORKDIR /workspace
|
||||
@@ -72,8 +73,12 @@ RUN chmod +x /usr/local/bin/init-firewall.sh && \
|
||||
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
|
||||
chmod 0440 /etc/sudoers.d/node-firewall
|
||||
|
||||
# Create scripts directory and copy unified entrypoint script
|
||||
RUN mkdir -p /scripts/runtime
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /scripts/runtime/claudecode-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh && \
|
||||
chmod +x /scripts/runtime/claudecode-entrypoint.sh
|
||||
|
||||
# Set the default shell to bash
|
||||
ENV SHELL /bin/zsh
|
||||
|
||||
141
QUICKSTART.md
Normal file
141
QUICKSTART.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 🚀 Quick Start Guide
|
||||
|
||||
Get Claude responding to your GitHub issues in minutes using Cloudflare Tunnel.
|
||||
|
||||
## Prerequisites
|
||||
- GitHub account
|
||||
- Docker installed
|
||||
- Claude.ai account with Max plan (5x or 20x)
|
||||
- Cloudflare account (free tier works)
|
||||
|
||||
## Step 1: Create a GitHub Bot Account
|
||||
|
||||
1. Sign out of GitHub and create a new account for your bot (e.g., `YourProjectBot`)
|
||||
2. In your main account, create a [Personal Access Token](https://github.com/settings/tokens) with `repo` and `write` permissions
|
||||
3. Add the bot account as a collaborator to your repositories
|
||||
|
||||
## Step 2: Clone and Configure
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/claude-did-this/claude-hub.git
|
||||
cd claude-hub
|
||||
|
||||
# Copy the quickstart environment file
|
||||
cp .env.quickstart .env
|
||||
|
||||
# Edit .env with your values
|
||||
nano .env
|
||||
```
|
||||
|
||||
Required values:
|
||||
- `GITHUB_TOKEN`: Your GitHub Personal Access Token
|
||||
- `GITHUB_WEBHOOK_SECRET`: Generate with `openssl rand -hex 32`
|
||||
- `BOT_USERNAME`: Your bot's GitHub username (e.g., `@YourProjectBot`)
|
||||
- `BOT_EMAIL`: Your bot's email
|
||||
- `AUTHORIZED_USERS`: Comma-separated GitHub usernames who can use the bot
|
||||
|
||||
## Step 3: Authenticate Claude
|
||||
|
||||
```bash
|
||||
# Run the interactive setup
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Open your browser for Claude.ai authentication
|
||||
2. Save your credentials securely
|
||||
3. Confirm everything is working
|
||||
|
||||
## Step 4: Start the Service
|
||||
|
||||
```bash
|
||||
# Start the webhook service
|
||||
docker compose up -d
|
||||
|
||||
# Check it's running
|
||||
docker compose logs -f webhook
|
||||
```
|
||||
|
||||
## Step 5: Install Cloudflare Tunnel
|
||||
|
||||
### Option A: Ubuntu/Debian
|
||||
```bash
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared focal main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||
|
||||
# Install cloudflared
|
||||
sudo apt-get update && sudo apt-get install cloudflared
|
||||
```
|
||||
|
||||
### Option B: Direct Download
|
||||
```bash
|
||||
# Download the latest cloudflared binary
|
||||
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
|
||||
sudo dpkg -i cloudflared-linux-amd64.deb
|
||||
```
|
||||
|
||||
### Option C: Using snap
|
||||
```bash
|
||||
sudo snap install cloudflared
|
||||
```
|
||||
|
||||
## Step 6: Create Tunnel
|
||||
|
||||
```bash
|
||||
# Create a tunnel to your local service
|
||||
cloudflared tunnel --url http://localhost:3002
|
||||
```
|
||||
|
||||
Copy the generated URL (like `https://abc123.trycloudflare.com`)
|
||||
|
||||
## Step 7: Configure GitHub Webhook
|
||||
|
||||
1. Go to your repository → Settings → Webhooks
|
||||
2. Click "Add webhook"
|
||||
3. **Payload URL**: Your Cloudflare URL + `/api/webhooks/github`
|
||||
- Example: `https://abc123.trycloudflare.com/api/webhooks/github`
|
||||
4. **Content type**: `application/json`
|
||||
5. **Secret**: Same value as `GITHUB_WEBHOOK_SECRET` in your .env
|
||||
6. **Events**: Select "Let me select individual events"
|
||||
- Check: Issues, Issue comments, Pull requests, Pull request reviews
|
||||
|
||||
## 🎉 You're Done!
|
||||
|
||||
Test it in your own repository by creating an issue and mentioning your bot:
|
||||
|
||||
```
|
||||
@YourProjectBot Can you help me understand this codebase?
|
||||
```
|
||||
|
||||
**Note:** Your bot will only respond in repositories where you've configured the webhook and to users listed in `AUTHORIZED_USERS`.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Production Deployment**: Set up a permanent Cloudflare Tunnel with `cloudflared service install`
|
||||
- **Advanced Features**: Check `.env.example` for PR auto-review, auto-tagging, and more
|
||||
- **Multiple Repos**: Add the same webhook to any repo where you want bot assistance
|
||||
|
||||
## Community & Support
|
||||
|
||||
[](https://discord.gg/yb7hwQjTFg)
|
||||
[](https://claude-did-this.com/claude-hub/overview)
|
||||
|
||||
Join our Discord server for help, updates, and to share your experience!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bot not responding?**
|
||||
- Check logs: `docker compose logs webhook`
|
||||
- Verify webhook delivery in GitHub → Settings → Webhooks → Recent Deliveries
|
||||
- Ensure the commenting user is in `AUTHORIZED_USERS`
|
||||
|
||||
**Authentication issues?**
|
||||
- Re-run: `./scripts/setup/setup-claude-interactive.sh`
|
||||
- Ensure you have an active Claude.ai Max plan (5x or 20x)
|
||||
|
||||
**Need help?** Ask in our [Discord server](https://discord.gg/yb7hwQjTFg) or check the [full documentation](https://claude-did-this.com/claude-hub/overview)!
|
||||
@@ -5,7 +5,7 @@ A webhook service that enables Claude AI to respond to GitHub mentions and execu
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
docker pull intelligenceassist/claude-github-webhook:latest
|
||||
docker pull intelligenceassist/claude-hub:latest
|
||||
|
||||
docker run -d \
|
||||
-p 8082:3002 \
|
||||
@@ -15,7 +15,7 @@ docker run -d \
|
||||
-e ANTHROPIC_API_KEY=your_anthropic_key \
|
||||
-e BOT_USERNAME=@YourBotName \
|
||||
-e AUTHORIZED_USERS=user1,user2 \
|
||||
intelligenceassist/claude-github-webhook:latest
|
||||
intelligenceassist/claude-hub:latest
|
||||
```
|
||||
|
||||
## Features
|
||||
@@ -34,7 +34,7 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
claude-webhook:
|
||||
image: intelligenceassist/claude-github-webhook:latest
|
||||
image: intelligenceassist/claude-hub:latest
|
||||
ports:
|
||||
- "8082:3002"
|
||||
volumes:
|
||||
@@ -84,9 +84,9 @@ Mention your bot in any issue or PR comment:
|
||||
|
||||
## Links
|
||||
|
||||
- [GitHub Repository](https://github.com/intelligence-assist/claude-github-webhook)
|
||||
- [Documentation](https://github.com/intelligence-assist/claude-github-webhook/tree/main/docs)
|
||||
- [Issue Tracker](https://github.com/intelligence-assist/claude-github-webhook/issues)
|
||||
- [GitHub Repository](https://github.com/intelligence-assist/claude-hub)
|
||||
- [Documentation](https://github.com/intelligence-assist/claude-hub/tree/main/docs)
|
||||
- [Issue Tracker](https://github.com/intelligence-assist/claude-hub/issues)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
736
README.md
736
README.md
@@ -1,401 +1,389 @@
|
||||
# Claude GitHub Webhook
|
||||
|
||||
[](https://github.com/intelligence-assist/claude-hub/actions/workflows/ci.yml)
|
||||
[](https://github.com/intelligence-assist/claude-hub/actions/workflows/security.yml)
|
||||
[](https://discord.com/widget?id=1377708770209304676&theme=dark)
|
||||
[](https://github.com/claude-did-this/claude-hub/actions/workflows/main.yml)
|
||||
[](https://github.com/claude-did-this/claude-hub/actions/workflows/security.yml)
|
||||
[](test/README.md)
|
||||
[](./coverage/index.html)
|
||||
[](https://codecov.io/gh/claude-did-this/claude-hub)
|
||||
[](https://github.com/claude-did-this/claude-hub/releases)
|
||||
[](https://hub.docker.com/r/intelligenceassist/claude-hub)
|
||||
[](package.json)
|
||||
[](LICENSE)
|
||||
|
||||
A webhook service that enables Claude Code to respond to GitHub mentions and execute commands within repository contexts. This microservice allows Claude to analyze code, answer questions, and optionally make changes when mentioned in GitHub comments.
|
||||
🚀 **[Quick Start Guide](./QUICKSTART.md)** | 💬 **[Discord](https://discord.com/widget?id=1377708770209304676&theme=dark)** | 📚 **[Documentation](https://claude-did-this.com/claude-hub/overview)** | 📖 **[Complete Setup](./docs/complete-workflow.md)** | 🔐 **[Authentication](./docs/claude-authentication-guide.md)**
|
||||
|
||||
## ⚡ Performance Optimizations
|
||||

|
||||
|
||||
This repository uses highly optimized CI/CD pipelines:
|
||||
- **Parallel test execution** for faster feedback loops
|
||||
- **Conditional Docker builds** (only when code/Dockerfile changes)
|
||||
- **Strategic runner distribution** (GitHub for tests, self-hosted for heavy builds)
|
||||
- **Advanced caching strategies** for significantly faster subsequent builds
|
||||
- **Build performance profiling** with timing and size metrics
|
||||
Deploy Claude Code as a fully autonomous GitHub bot. Create your own bot account, mention it in any issue or PR, and watch AI-powered development happen end-to-end. Claude can implement complete features, review code, merge PRs, wait for CI builds, and run for hours autonomously until tasks are completed. Production-ready microservice with container isolation, automated workflows, and intelligent project management.
|
||||
|
||||
## Documentation
|
||||
|
||||
For comprehensive documentation, see:
|
||||
- [Complete Workflow Guide](./docs/complete-workflow.md) - Full technical workflow documentation
|
||||
- [GitHub Integration](./docs/github-workflow.md) - GitHub-specific features and setup
|
||||
- [Container Setup](./docs/container-setup.md) - Docker container configuration
|
||||
- [Container Limitations](./docs/container-limitations.md) - Known constraints and workarounds
|
||||
- [AWS Authentication Best Practices](./docs/aws-authentication-best-practices.md) - Secure AWS credential management
|
||||
- [Scripts Documentation](./SCRIPTS.md) - Organized scripts and their usage
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Trigger Claude when mentioned in GitHub comments with your configured bot username
|
||||
- Allow Claude to research repository code and answer questions
|
||||
- Direct API access for Claude without GitHub webhook requirements
|
||||
- Stateless container execution mode for isolation and scalability
|
||||
- Optionally permit Claude to make code changes when requested
|
||||
|
||||
## 🚀 Setup Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16 or higher
|
||||
- Docker and Docker Compose
|
||||
- GitHub account with access to the repositories you want to use
|
||||
|
||||
### Quick Setup
|
||||
|
||||
1. **Clone this repository**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/claude-github-webhook.git
|
||||
cd claude-github-webhook
|
||||
```
|
||||
|
||||
2. **Setup secure credentials**
|
||||
```bash
|
||||
./scripts/setup/setup-secure-credentials.sh
|
||||
```
|
||||
This creates secure credential files with proper permissions.
|
||||
|
||||
3. **Start the service**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
The service will be available at `http://localhost:8082`
|
||||
|
||||
### Manual Configuration (Alternative)
|
||||
|
||||
If you prefer to configure manually instead of using the setup script:
|
||||
```
|
||||
cp .env.example .env
|
||||
nano .env # or use your preferred editor
|
||||
```
|
||||
|
||||
**a. GitHub Webhook Secret**
|
||||
- Generate a secure random string to use as your webhook secret
|
||||
- You can use this command to generate one:
|
||||
```
|
||||
node -e "console.log(require('crypto').randomBytes(20).toString('hex'))"
|
||||
```
|
||||
- Save this value in your `.env` file as `GITHUB_WEBHOOK_SECRET`
|
||||
- You'll use this same value when setting up the webhook in GitHub
|
||||
|
||||
**b. GitHub Personal Access Token**
|
||||
- Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
|
||||
- Click "Generate new token"
|
||||
- Name your token (e.g., "Claude GitHub Webhook")
|
||||
- Set the expiration as needed
|
||||
- Select the repositories you want Claude to access
|
||||
- Under "Repository permissions":
|
||||
- Issues: Read and write (to post comments)
|
||||
- Contents: Read (to read repository code)
|
||||
- Click "Generate token"
|
||||
- Copy the generated token to your `.env` file as `GITHUB_TOKEN`
|
||||
|
||||
**c. AWS Credentials (for Claude via Bedrock)**
|
||||
- You need AWS Bedrock credentials to access Claude
|
||||
- Update the following values in your `.env` file:
|
||||
```
|
||||
AWS_ACCESS_KEY_ID=your_aws_access_key
|
||||
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
|
||||
AWS_REGION=us-east-1
|
||||
CLAUDE_CODE_USE_BEDROCK=1
|
||||
ANTHROPIC_MODEL=anthropic.claude-3-sonnet-20240229-v1:0
|
||||
```
|
||||
- Note: You don't need a Claude/Anthropic API key when using Bedrock
|
||||
|
||||
**d. Bot Configuration**
|
||||
- Set the `BOT_USERNAME` environment variable in your `.env` file to the GitHub mention you want to use
|
||||
- This setting is required to prevent infinite loops
|
||||
- Example: `BOT_USERNAME=@MyBot`
|
||||
- No default is provided - this must be explicitly configured
|
||||
- Set `BOT_EMAIL` for the email address used in git commits made by the bot
|
||||
- Set `DEFAULT_AUTHORIZED_USER` to specify the default GitHub username authorized to use the bot
|
||||
- Use `AUTHORIZED_USERS` for a comma-separated list of GitHub usernames allowed to use the bot
|
||||
|
||||
**e. Server Port and Other Settings**
|
||||
- By default, the server runs on port 3000
|
||||
- To use a different port, set the `PORT` environment variable in your `.env` file
|
||||
- Set `DEFAULT_GITHUB_OWNER` and `DEFAULT_GITHUB_USER` for CLI defaults when using the webhook CLI
|
||||
- Set `TEST_REPO_FULL_NAME` to configure the default repository for test scripts
|
||||
- Review other settings in the `.env` file for customization options
|
||||
|
||||
**AWS Credentials**: The service now supports multiple AWS authentication methods:
|
||||
- **Instance Profiles** (EC2): Automatically uses instance metadata
|
||||
- **Task Roles** (ECS): Automatically uses container credentials
|
||||
- **Temporary Credentials**: Set `AWS_SESSION_TOKEN` for STS credentials
|
||||
- **Static Credentials**: Fall back to `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
|
||||
|
||||
For migration from static credentials, run:
|
||||
```
|
||||
./scripts/aws/migrate-aws-credentials.sh
|
||||
```
|
||||
|
||||
4. **Start the server**
|
||||
```
|
||||
npm start
|
||||
```
|
||||
For development with auto-restart:
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### GitHub Webhook Configuration
|
||||
|
||||
1. **Go to your GitHub repository**
|
||||
2. **Navigate to Settings → Webhooks**
|
||||
3. **Click "Add webhook"**
|
||||
4. **Configure the webhook:**
|
||||
- Payload URL: `https://claude.jonathanflatt.org/api/webhooks/github`
|
||||
- Content type: `application/json`
|
||||
- Secret: The same value you set for `GITHUB_WEBHOOK_SECRET` in your `.env` file
|
||||
- Events: Select "Send me everything" if you want to handle multiple event types, or choose specific events
|
||||
- Active: Check this box to enable the webhook
|
||||
5. **Click "Add webhook"**
|
||||
|
||||
### Testing Your Setup
|
||||
|
||||
1. **Verify the webhook is receiving events**
|
||||
- After setting up the webhook, GitHub will send a ping event
|
||||
- Check your server logs to confirm it's receiving events
|
||||
|
||||
2. **Test with a sample comment**
|
||||
- Create a new issue or pull request in your repository
|
||||
- Add a comment mentioning your configured bot username followed by a question, like:
|
||||
```
|
||||
@MyBot What does this repository do?
|
||||
```
|
||||
(Replace @MyBot with your configured BOT_USERNAME)
|
||||
- Claude should respond with a new comment in the thread
|
||||
|
||||
3. **Using the test utilities**
|
||||
- You can use the included test utility to verify your webhook setup:
|
||||
```
|
||||
node test-outgoing-webhook.js
|
||||
```
|
||||
- This will start a test server and provide instructions for testing
|
||||
|
||||
- To test the direct Claude API:
|
||||
```
|
||||
node test-claude-api.js owner/repo
|
||||
```
|
||||
- To test the container-based execution:
|
||||
```
|
||||
./scripts/build/build.sh claudecode # First build the container
|
||||
node test-claude-api.js owner/repo container "Your command here"
|
||||
```
|
||||
|
||||
## Automated PR Review
|
||||
|
||||
The webhook service includes an intelligent automated PR review system that triggers comprehensive code reviews when all CI checks pass successfully.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Trigger**: When a `check_suite` webhook event is received with `conclusion: 'success'`
|
||||
2. **Validation**: The system queries GitHub's Combined Status API to verify **all** required status checks have passed
|
||||
3. **Review**: Only when all checks are successful, Claude performs a comprehensive PR review
|
||||
4. **Output**: Detailed review comments, line-specific feedback, and approval/change requests
|
||||
|
||||
### Review Process
|
||||
|
||||
When triggered, Claude automatically:
|
||||
|
||||
- **Analyzes PR changes**: Reviews all modified files and their context
|
||||
- **Security assessment**: Checks for potential vulnerabilities, injection attacks, authentication issues
|
||||
- **Logic review**: Identifies bugs, edge cases, and potential runtime errors
|
||||
- **Performance evaluation**: Flags inefficient algorithms and unnecessary computations
|
||||
- **Code quality**: Reviews organization, maintainability, and adherence to best practices
|
||||
- **Error handling**: Verifies proper exception handling and edge case coverage
|
||||
- **Test coverage**: Assesses test quality and effectiveness
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Prevents duplicate reviews**: Uses Combined Status API to ensure reviews only happen once all checks complete
|
||||
- **Comprehensive analysis**: Covers security, performance, logic, and maintainability
|
||||
- **Line-specific feedback**: Provides targeted comments on specific code lines when issues are found
|
||||
- **Professional tone**: Balances constructive criticism with positive reinforcement
|
||||
- **Approval workflow**: Concludes with either approval or change requests based on findings
|
||||
|
||||
### Configuration
|
||||
|
||||
The automated PR review system is enabled by default and requires:
|
||||
|
||||
- `check_suite` webhook events (included in "Send me everything")
|
||||
- `pull_request` webhook events for PR context
|
||||
- GitHub token with appropriate repository permissions
|
||||
|
||||
### Supported Events
|
||||
|
||||
The webhook service responds to these GitHub events:
|
||||
|
||||
- **`issue_comment`**: Manual Claude mentions in issue/PR comments
|
||||
- **`pull_request_review_comment`**: Manual Claude mentions in PR review comments
|
||||
- **`issues` (opened)**: Automatic issue labeling and analysis
|
||||
- **`check_suite` (completed)**: Automated PR reviews when all CI checks pass
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See the [Complete Workflow Guide](./docs/complete-workflow.md#troubleshooting) for detailed troubleshooting information.
|
||||
|
||||
### Quick Checks
|
||||
- Verify webhook signature matches
|
||||
- Check Docker daemon is running
|
||||
- Confirm AWS/Bedrock credentials are valid
|
||||
- Ensure GitHub token has correct permissions
|
||||
|
||||
## Security: Pre-commit Hooks
|
||||
|
||||
This project includes pre-commit hooks that automatically scan for credentials and secrets before commits. This helps prevent accidental exposure of sensitive information.
|
||||
|
||||
### Features
|
||||
|
||||
- **Credential Detection**: Scans for AWS keys, GitHub tokens, API keys, and other secrets
|
||||
- **Multiple Scanners**: Uses both `detect-secrets` and `gitleaks` for comprehensive coverage
|
||||
- **Code Quality**: Also includes hooks for trailing whitespace, JSON/YAML validation, and more
|
||||
|
||||
### Usage
|
||||
|
||||
Pre-commit hooks are automatically installed when you run `./scripts/setup/setup.sh`. They run automatically on every commit.
|
||||
|
||||
To manually run the hooks:
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
For more information, see [pre-commit setup documentation](./docs/pre-commit-setup.md).
|
||||
|
||||
## Direct Claude API
|
||||
|
||||
The server provides a direct API endpoint for Claude that doesn't rely on GitHub webhooks. This allows you to integrate Claude with other systems or test Claude's responses.
|
||||
|
||||
### API Endpoint
|
||||
|
||||
```
|
||||
POST /api/claude
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| repoFullName | string | The repository name in the format "owner/repo" |
|
||||
| command | string | The command or question to send to Claude |
|
||||
| authToken | string | Optional authentication token (required if CLAUDE_API_AUTH_REQUIRED=1) |
|
||||
| useContainer | boolean | Whether to use container-based execution (optional, defaults to false) |
|
||||
|
||||
### Example Request
|
||||
|
||||
```json
|
||||
{
|
||||
"repoFullName": "owner/repo",
|
||||
"command": "Explain what this repository does",
|
||||
"authToken": "your-auth-token",
|
||||
"useContainer": true
|
||||
}
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Command processed successfully",
|
||||
"response": "This repository is a webhook server that integrates Claude with GitHub..."
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
To secure the API, you can enable authentication by setting the following environment variables:
|
||||
|
||||
```
|
||||
CLAUDE_API_AUTH_REQUIRED=1
|
||||
CLAUDE_API_AUTH_TOKEN=your-secret-token
|
||||
```
|
||||
|
||||
### Container-Based Execution
|
||||
|
||||
The container-based execution mode provides isolation and better scalability. When enabled, each request will:
|
||||
|
||||
1. Launch a new Docker container with Claude Code CLI
|
||||
2. Clone the repository inside the container (or use cached repository)
|
||||
3. Analyze the repository structure and content
|
||||
4. Generate a helpful response based on the analysis
|
||||
5. Clean up resources
|
||||
|
||||
> Note: Due to technical limitations with running Claude in containers, the current implementation uses automatic repository analysis instead of direct Claude execution. See [Container Limitations](./docs/container-limitations.md) for details.
|
||||
|
||||
To enable container-based execution:
|
||||
|
||||
1. Build the Claude container:
|
||||
```
|
||||
./scripts/build/build.sh claude
|
||||
```
|
||||
|
||||
2. Set the environment variables:
|
||||
```
|
||||
CLAUDE_USE_CONTAINERS=1
|
||||
CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
REPO_CACHE_DIR=/path/to/cache # Optional
|
||||
REPO_CACHE_MAX_AGE_MS=3600000 # Optional, defaults to 1 hour (in milliseconds)
|
||||
CONTAINER_LIFETIME_MS=7200000 # Optional, container execution timeout in milliseconds (defaults to 2 hours)
|
||||
```
|
||||
|
||||
### Container Test Utility
|
||||
|
||||
A dedicated test script is provided for testing container execution directly:
|
||||
## What This Does
|
||||
|
||||
```bash
|
||||
./test/container/test-container.sh
|
||||
# In any GitHub issue or PR (using your configured bot account):
|
||||
@YourBotName implement user authentication with OAuth
|
||||
@YourBotName review this PR for security vulnerabilities
|
||||
@YourBotName fix the failing CI tests and merge when ready
|
||||
@YourBotName refactor the database layer for better performance
|
||||
```
|
||||
|
||||
This utility will:
|
||||
1. Force container mode
|
||||
2. Execute the command in a container
|
||||
3. Display the Claude response
|
||||
4. Show execution timing information
|
||||
Claude autonomously handles complete development workflows. It analyzes your entire repository, implements features from scratch, conducts thorough code reviews, manages pull requests, monitors CI/CD pipelines, and responds to automated feedback - all without human intervention. No context switching. No manual oversight required. Just seamless autonomous development where you work.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
**Follow our [10-minute Quick Start Guide](./QUICKSTART.md)** to get Claude responding to your GitHub issues using Cloudflare Tunnel - no domain or complex setup required!
|
||||
|
||||
```bash
|
||||
# 1. Clone and configure
|
||||
git clone https://github.com/claude-did-this/claude-hub.git
|
||||
cd claude-hub
|
||||
cp .env.quickstart .env
|
||||
nano .env # Add your GitHub token and bot details
|
||||
|
||||
# 2. Authenticate Claude (uses your Claude.ai Max subscription)
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
# 3. Start the service
|
||||
docker compose up -d
|
||||
|
||||
# 4. Create a tunnel (see quickstart guide for details)
|
||||
cloudflared tunnel --url http://localhost:3002
|
||||
```
|
||||
|
||||
That's it! Your bot is ready to use. See the **[complete quickstart guide](./QUICKSTART.md)** for detailed instructions and webhook setup.
|
||||
|
||||
## Autonomous Workflow Capabilities
|
||||
|
||||
### End-to-End Development 🚀
|
||||
- **Feature Implementation**: From requirements to fully tested, production-ready code
|
||||
- **Code Review & Quality**: Comprehensive analysis including security, performance, and best practices
|
||||
- **PR Lifecycle Management**: Creates branches, commits changes, pushes code, and manages merge process
|
||||
- **CI/CD Monitoring**: Actively waits for builds, analyzes test results, and fixes failures
|
||||
- **Automated Code Response**: Responds to automated review comments and adapts based on feedback
|
||||
|
||||
### Intelligent Task Management 🧠
|
||||
- **Multi-hour Operations**: Continues working autonomously until complex tasks are 100% complete
|
||||
- **Dependency Resolution**: Handles blockers, waits for external processes, and resumes work automatically
|
||||
- **Context Preservation**: Maintains project state and progress across long-running operations
|
||||
- **Adaptive Problem Solving**: Iterates on solutions based on test results and code review feedback
|
||||
|
||||
## Key Features
|
||||
|
||||
### Autonomous Development 🤖
|
||||
- **Complete Feature Implementation**: Claude codes entire features from requirements to deployment
|
||||
- **Intelligent PR Management**: Automatically creates, reviews, and merges pull requests
|
||||
- **CI/CD Integration**: Waits for builds, responds to test failures, and handles automated workflows
|
||||
- **Long-running Tasks**: Operates autonomously for hours until complex projects are completed
|
||||
- **Auto-labeling**: New issues automatically tagged by content analysis
|
||||
- **Context-aware**: Claude understands your entire repository structure and development patterns
|
||||
- **Stateless execution**: Each request runs in isolated Docker containers
|
||||
|
||||
### Performance Architecture ⚡
|
||||
- Parallel test execution with strategic runner distribution
|
||||
- Conditional Docker builds (only when code changes)
|
||||
- Repository caching for sub-second response times
|
||||
- Advanced build profiling with timing metrics
|
||||
|
||||
### Enterprise Security 🔒
|
||||
- Webhook signature verification (HMAC-SHA256)
|
||||
- AWS IAM role-based authentication
|
||||
- Pre-commit credential scanning
|
||||
- Container isolation with minimal permissions
|
||||
- Fine-grained GitHub token scoping
|
||||
|
||||
|
||||
## Bot Account Setup
|
||||
|
||||
**Current Setup**: You need to create your own GitHub bot account:
|
||||
|
||||
1. **Create a dedicated GitHub account** for your bot (e.g., `MyProjectBot`)
|
||||
2. **Generate a Personal Access Token** with repository permissions
|
||||
3. **Configure the bot username** in your environment variables
|
||||
4. **Add the bot account** as a collaborator to your repositories
|
||||
|
||||
**Future Release**: We plan to release this as a GitHub App that provides a universal bot account, eliminating the need for individual bot setup while maintaining the same functionality for self-hosted instances.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### 1. Environment Configuration
|
||||
|
||||
```bash
|
||||
# Core settings
|
||||
BOT_USERNAME=YourBotName # GitHub bot account username (create your own bot account)
|
||||
GITHUB_WEBHOOK_SECRET=<generated> # Webhook validation
|
||||
GITHUB_TOKEN=<fine-grained-pat> # Repository access (from your bot account)
|
||||
|
||||
# Claude Authentication - Choose ONE method:
|
||||
|
||||
# Option 1: Setup Container (Personal/Development)
|
||||
# Use existing Claude Max subscription (5x or 20x plans)
|
||||
# See docs/setup-container-guide.md for setup
|
||||
|
||||
# Option 2: Direct API Key (Production/Team)
|
||||
ANTHROPIC_API_KEY=sk-ant-your-api-key
|
||||
|
||||
# Option 3: AWS Bedrock (Enterprise)
|
||||
AWS_REGION=us-east-1
|
||||
ANTHROPIC_MODEL=anthropic.claude-3-sonnet-20240229-v1:0
|
||||
CLAUDE_CODE_USE_BEDROCK=1
|
||||
|
||||
# Security
|
||||
AUTHORIZED_USERS=user1,user2,user3 # Allowed GitHub usernames
|
||||
CLAUDE_API_AUTH_REQUIRED=1 # Enable API authentication
|
||||
```
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Setup Container (Personal/Development)
|
||||
Use your existing Claude Max subscription for automation instead of pay-per-use API fees:
|
||||
|
||||
```bash
|
||||
# 1. Run interactive authentication setup
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
# 2. In container: authenticate with your subscription
|
||||
claude --dangerously-skip-permissions # Follow authentication flow
|
||||
exit # Save authentication
|
||||
|
||||
# 3. Use captured authentication
|
||||
cp -r ${CLAUDE_HUB_DIR:-~/.claude-hub}/* ~/.claude/
|
||||
```
|
||||
|
||||
**Prerequisites**: Claude Max subscription (5x or 20x plans). Claude Pro does not include Claude Code access.
|
||||
**Details**: [Setup Container Guide](./docs/setup-container-guide.md)
|
||||
|
||||
### Direct API Key (Production/Team)
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
|
||||
```
|
||||
|
||||
**Best for**: Production environments, team usage, guaranteed stability.
|
||||
**Details**: [Authentication Guide](./docs/claude-authentication-guide.md)
|
||||
|
||||
### AWS Bedrock (Enterprise)
|
||||
```bash
|
||||
AWS_REGION=us-east-1
|
||||
ANTHROPIC_MODEL=anthropic.claude-3-sonnet-20240229-v1:0
|
||||
CLAUDE_CODE_USE_BEDROCK=1
|
||||
```
|
||||
|
||||
**Best for**: Enterprise deployments, AWS integration, compliance requirements.
|
||||
**Details**: [Authentication Guide](./docs/claude-authentication-guide.md)
|
||||
|
||||
### 2. GitHub Webhook Setup
|
||||
|
||||
1. Navigate to Repository → Settings → Webhooks
|
||||
2. Add webhook:
|
||||
- **Payload URL**: `https://your-domain.com/api/webhooks/github`
|
||||
- **Content type**: `application/json`
|
||||
- **Secret**: Your `GITHUB_WEBHOOK_SECRET`
|
||||
- **Events**: Select "Send me everything"
|
||||
|
||||
### 3. AWS Authentication Options
|
||||
|
||||
```bash
|
||||
# Option 1: IAM Instance Profile (EC2)
|
||||
# Automatically uses instance metadata
|
||||
|
||||
# Option 2: ECS Task Role
|
||||
# Automatically uses container credentials
|
||||
|
||||
# Option 3: AWS Profile
|
||||
./scripts/aws/setup-aws-profiles.sh
|
||||
|
||||
# Option 4: Static Credentials (not recommended)
|
||||
AWS_ACCESS_KEY_ID=xxx
|
||||
AWS_SECRET_ACCESS_KEY=xxx
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Direct API Access
|
||||
|
||||
Integrate Claude without GitHub webhooks:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/claude \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"repoFullName": "owner/repo",
|
||||
"command": "Analyze security vulnerabilities",
|
||||
"authToken": "your-token",
|
||||
"useContainer": true
|
||||
}'
|
||||
```
|
||||
|
||||
### CLI Tool
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
./cli/claude-webhook myrepo "Review the authentication flow"
|
||||
|
||||
# PR review
|
||||
./cli/claude-webhook owner/repo "Review this PR" -p -b feature-branch
|
||||
|
||||
# Specific issue
|
||||
./cli/claude-webhook myrepo "Fix this bug" -i 42
|
||||
```
|
||||
|
||||
### Container Execution Modes
|
||||
|
||||
Different operations use tailored security profiles for autonomous execution:
|
||||
|
||||
- **Auto-tagging**: Minimal permissions (Read + GitHub tools only)
|
||||
- **PR Reviews**: Standard permissions (full tool access with automated merge capabilities)
|
||||
- **Feature Development**: Full development permissions (code editing, testing, CI monitoring)
|
||||
- **Long-running Tasks**: Extended container lifetime with checkpoint/resume functionality
|
||||
- **Custom Commands**: Configurable via `--allowedTools` flag
|
||||
|
||||
## Architecture Deep Dive
|
||||
|
||||
### Autonomous Request Flow
|
||||
|
||||
```
|
||||
GitHub Event → Webhook Endpoint → Signature Verification
|
||||
↓ ↓
|
||||
Container Spawn ← Command Parser ← Event Processor
|
||||
↓
|
||||
Claude Analysis → Feature Implementation → Testing & CI
|
||||
↓ ↓ ↓
|
||||
GitHub API ← Code Review ← PR Management ← Build Monitoring
|
||||
↓
|
||||
Autonomous Merge/Deploy → Task Completion
|
||||
```
|
||||
|
||||
### Autonomous Container Lifecycle
|
||||
|
||||
1. **Spawn**: New Docker container per request with extended lifetime for long tasks
|
||||
2. **Clone**: Repository fetched (or cache hit) with full development setup
|
||||
3. **Execute**: Claude implements features, runs tests, monitors CI, handles feedback autonomously
|
||||
4. **Iterate**: Continuous development cycle until task completion
|
||||
5. **Deploy**: Results pushed, PRs merged, tasks marked complete
|
||||
6. **Cleanup**: Container destroyed after successful task completion
|
||||
|
||||
### Security Layers
|
||||
|
||||
- **Network**: Webhook signature validation
|
||||
- **Authentication**: GitHub user allowlist
|
||||
- **Authorization**: Fine-grained token permissions
|
||||
- **Execution**: Container isolation
|
||||
- **Tools**: Operation-specific allowlists
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Repository Caching
|
||||
|
||||
The container mode includes an intelligent repository caching mechanism:
|
||||
|
||||
- Repositories are cached to improve performance for repeated queries
|
||||
- Cache is automatically refreshed after the configured expiration time
|
||||
- You can configure the cache location and max age via environment variables:
|
||||
```
|
||||
REPO_CACHE_DIR=/path/to/cache
|
||||
REPO_CACHE_MAX_AGE_MS=3600000 # 1 hour in milliseconds
|
||||
```
|
||||
|
||||
For detailed information about container mode setup and usage, see [Container Setup Documentation](./docs/container-setup.md).
|
||||
|
||||
## Development
|
||||
|
||||
To run the server in development mode with auto-restart:
|
||||
|
||||
```bash
|
||||
REPO_CACHE_DIR=/cache/repos
|
||||
REPO_CACHE_MAX_AGE_MS=3600000 # 1 hour
|
||||
```
|
||||
|
||||
### Container Optimization
|
||||
|
||||
```bash
|
||||
CONTAINER_LIFETIME_MS=7200000 # 2 hour timeout
|
||||
CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
|
||||
- Parallel Jest test execution
|
||||
- Docker layer caching
|
||||
- Conditional image builds
|
||||
- Self-hosted runners for heavy operations
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
### Logs
|
||||
```bash
|
||||
docker compose logs -f webhook
|
||||
```
|
||||
|
||||
### Test Suite
|
||||
```bash
|
||||
npm test # All tests
|
||||
npm run test:unit # Unit only
|
||||
npm run test:integration # Integration only
|
||||
npm run test:coverage # With coverage report
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
DEBUG=claude:* npm run dev
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Deep Dive Guides
|
||||
- [Setup Container Authentication](./docs/setup-container-guide.md) - Technical details for subscription-based auth
|
||||
- [Authentication Guide](./docs/claude-authentication-guide.md) - All authentication methods and troubleshooting
|
||||
- [Complete Workflow](./docs/complete-workflow.md) - End-to-end technical guide
|
||||
- [Container Setup](./docs/container-setup.md) - Docker configuration details
|
||||
- [AWS Best Practices](./docs/aws-authentication-best-practices.md) - IAM and credential management
|
||||
- [GitHub Integration](./docs/github-workflow.md) - Webhook events and permissions
|
||||
|
||||
### Reference
|
||||
- [Scripts Documentation](./docs/SCRIPTS.md) - Utility scripts and commands
|
||||
- [Command Reference](./CLAUDE.md) - Build and run commands
|
||||
|
||||
## Contributing
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Setup pre-commit hooks
|
||||
./scripts/setup/setup-precommit.sh
|
||||
|
||||
# Run in dev mode
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
### Code Standards
|
||||
|
||||
Run tests with:
|
||||
- Node.js 20+ with async/await patterns
|
||||
- Jest for testing with >80% coverage target
|
||||
- ESLint + Prettier for code formatting
|
||||
- Conventional commits for version management
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
### Security Checklist
|
||||
|
||||
# Run only unit tests
|
||||
npm run test:unit
|
||||
- [ ] No hardcoded credentials
|
||||
- [ ] All inputs sanitized
|
||||
- [ ] Webhook signatures verified
|
||||
- [ ] Container permissions minimal
|
||||
- [ ] Logs redact sensitive data
|
||||
|
||||
# Run only integration tests
|
||||
npm run test:integration
|
||||
## Troubleshooting
|
||||
|
||||
# Run only E2E tests
|
||||
npm run test:e2e
|
||||
### Common Issues
|
||||
|
||||
# Run tests with coverage report
|
||||
npm run test:coverage
|
||||
```
|
||||
**Webhook not responding**
|
||||
- Verify signature secret matches
|
||||
- Check GitHub token permissions
|
||||
- Confirm webhook URL is accessible
|
||||
|
||||
See [Test Documentation](test/README.md) for more details on the testing framework.
|
||||
**Claude timeouts**
|
||||
- Increase `CONTAINER_LIFETIME_MS`
|
||||
- Check AWS Bedrock quotas
|
||||
- Verify network connectivity
|
||||
|
||||
**Permission denied**
|
||||
- Confirm user in `AUTHORIZED_USERS`
|
||||
- Check GitHub token scopes
|
||||
- Verify AWS IAM permissions
|
||||
|
||||
### Support
|
||||
|
||||
- Report issues: [GitHub Issues](https://github.com/claude-did-this/claude-hub/issues)
|
||||
- Detailed troubleshooting: [Complete Workflow Guide](./docs/complete-workflow.md#troubleshooting)
|
||||
|
||||
## License
|
||||
|
||||
MIT - See the [LICENSE file](LICENSE) for details.
|
||||
BIN
assets/brain_factory.png
Executable file
BIN
assets/brain_factory.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
12
babel.config.js
Normal file
12
babel.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: '20'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
{"parentUuid":null,"isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","type":"user","message":{"role":"user","content":"auth"},"uuid":"5bea393c-77c6-4f32-ac62-a157e0159045","timestamp":"2025-05-19T01:19:11.851Z"}
|
||||
{"parentUuid":"5bea393c-77c6-4f32-ac62-a157e0159045","isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","message":{"id":"msg_bdrk_01Lz7rrWgXdzbMayCabnExTJ","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"text","text":"I'll search for authentication-related files and code in the repository."},{"type":"tool_use","id":"toolu_bdrk_01FCr4cpVZtKEZ1E9TD6AXcr","name":"Task","input":{"description":"Find auth files","prompt":"Search for any authentication-related files, code, or implementations in the repository. Look for files with names containing \"auth\", authentication implementations, login functionality, or security-related code. Return a list of relevant files and a brief summary of what each one contains."}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":17318,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":136}},"costUSD":0.053994,"durationMs":5319,"type":"assistant","uuid":"5df3af64-5b6c-457f-b559-9741977e06f5","timestamp":"2025-05-19T01:19:17.209Z"}
|
||||
{"parentUuid":"5df3af64-5b6c-457f-b559-9741977e06f5","isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"[Request interrupted by user for tool use]","is_error":true,"tool_use_id":"toolu_bdrk_01FCr4cpVZtKEZ1E9TD6AXcr"}]},"uuid":"84e6bfdd-e508-459d-b0b8-d02ccada8f5f","timestamp":"2025-05-19T01:19:21.315Z","toolUseResult":"Error: [Request interrupted by user for tool use]"}
|
||||
{"parentUuid":"84e6bfdd-e508-459d-b0b8-d02ccada8f5f","isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user for tool use]"}]},"uuid":"ffe5b08f-786c-4cc7-9271-fead3ca72f4f","timestamp":"2025-05-19T01:19:21.319Z"}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
68
docker-compose.test.yml
Normal file
68
docker-compose.test.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Test runner service - runs tests in container
|
||||
test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: test
|
||||
cache_from:
|
||||
- ${DOCKER_HUB_ORGANIZATION:-intelligenceassist}/claude-hub:test-cache
|
||||
environment:
|
||||
- NODE_ENV=test
|
||||
- CI=true
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN:-test-token}
|
||||
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET:-test-secret}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-test-key}
|
||||
volumes:
|
||||
- ./coverage:/app/coverage
|
||||
# Run only unit tests in CI (no e2e tests that require Docker)
|
||||
command: npm run test:unit
|
||||
|
||||
# Integration test service
|
||||
integration-test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: test
|
||||
environment:
|
||||
- NODE_ENV=test
|
||||
- CI=true
|
||||
- TEST_SUITE=integration
|
||||
volumes:
|
||||
- ./coverage:/app/coverage
|
||||
command: npm run test:integration
|
||||
depends_on:
|
||||
- webhook
|
||||
|
||||
# Webhook service for integration testing
|
||||
webhook:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
environment:
|
||||
- NODE_ENV=test
|
||||
- PORT=3002
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN:-test-token}
|
||||
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET:-test-secret}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-test-key}
|
||||
ports:
|
||||
- "3002:3002"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# E2E test service - removed from CI, use for local development only
|
||||
# To run e2e tests locally with Docker access:
|
||||
# docker compose -f docker-compose.test.yml run --rm -v /var/run/docker.sock:/var/run/docker.sock e2e-test
|
||||
|
||||
# Networks
|
||||
networks:
|
||||
default:
|
||||
name: claude-hub-test
|
||||
driver: bridge
|
||||
@@ -2,48 +2,42 @@ services:
|
||||
webhook:
|
||||
build: .
|
||||
ports:
|
||||
- "8082:3002"
|
||||
- "${PORT:-3002}:${PORT:-3002}"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HOME}/.aws:/root/.aws:ro
|
||||
secrets:
|
||||
- github_token
|
||||
- anthropic_api_key
|
||||
- webhook_secret
|
||||
- ${HOME}/.claude-hub:/home/node/.claude
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3002
|
||||
- PORT=${PORT:-3002}
|
||||
- TRUST_PROXY=${TRUST_PROXY:-true}
|
||||
- AUTHORIZED_USERS=${AUTHORIZED_USERS:-Cheffromspace}
|
||||
- BOT_USERNAME=${BOT_USERNAME:-@MCPClaude}
|
||||
- BOT_EMAIL=${BOT_EMAIL:-claude@example.com}
|
||||
- DEFAULT_GITHUB_OWNER=${DEFAULT_GITHUB_OWNER:-Cheffromspace}
|
||||
- DEFAULT_GITHUB_USER=${DEFAULT_GITHUB_USER:-Cheffromspace}
|
||||
- DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
|
||||
- CLAUDE_USE_CONTAINERS=1
|
||||
- CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
# Point to secret files instead of env vars
|
||||
- GITHUB_TOKEN_FILE=/run/secrets/github_token
|
||||
- ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
|
||||
- GITHUB_WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
|
||||
- CLAUDE_AUTH_HOST_DIR=${CLAUDE_AUTH_HOST_DIR:-${HOME}/.claude-hub}
|
||||
- DISABLE_LOG_REDACTION=true
|
||||
# Claude Code timeout settings for unattended mode
|
||||
- BASH_DEFAULT_TIMEOUT_MS=${BASH_DEFAULT_TIMEOUT_MS:-600000} # 10 minutes default
|
||||
- BASH_MAX_TIMEOUT_MS=${BASH_MAX_TIMEOUT_MS:-1200000} # 20 minutes max
|
||||
# Smart wait for all meaningful checks by default, or use specific workflow trigger
|
||||
- PR_REVIEW_WAIT_FOR_ALL_CHECKS=${PR_REVIEW_WAIT_FOR_ALL_CHECKS:-true}
|
||||
- PR_REVIEW_TRIGGER_WORKFLOW=${PR_REVIEW_TRIGGER_WORKFLOW:-}
|
||||
- PR_REVIEW_DEBOUNCE_MS=${PR_REVIEW_DEBOUNCE_MS:-5000}
|
||||
- PR_REVIEW_MAX_WAIT_MS=${PR_REVIEW_MAX_WAIT_MS:-1800000}
|
||||
- PR_REVIEW_CONDITIONAL_TIMEOUT_MS=${PR_REVIEW_CONDITIONAL_TIMEOUT_MS:-300000}
|
||||
# Secrets from environment variables
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:${PORT:-3002}/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- n8n_default
|
||||
|
||||
secrets:
|
||||
github_token:
|
||||
file: ./secrets/github_token.txt
|
||||
anthropic_api_key:
|
||||
file: ./secrets/anthropic_api_key.txt
|
||||
webhook_secret:
|
||||
file: ./secrets/webhook_secret.txt
|
||||
|
||||
networks:
|
||||
n8n_default:
|
||||
external: true
|
||||
start_period: 10s
|
||||
102
docs/SCRIPTS.md
102
docs/SCRIPTS.md
@@ -9,25 +9,20 @@ This document provides an overview of the scripts in this repository, organized
|
||||
| `scripts/setup/setup.sh` | Main setup script for the project | `./scripts/setup/setup.sh` |
|
||||
| `scripts/setup/setup-precommit.sh` | Sets up pre-commit hooks | `./scripts/setup/setup-precommit.sh` |
|
||||
| `scripts/setup/setup-claude-auth.sh` | Sets up Claude authentication | `./scripts/setup/setup-claude-auth.sh` |
|
||||
| `scripts/setup/setup-new-repo.sh` | Sets up a new clean repository | `./scripts/setup/setup-new-repo.sh` |
|
||||
| `scripts/setup/create-new-repo.sh` | Creates a new repository | `./scripts/setup/create-new-repo.sh` |
|
||||
| `scripts/setup/setup-secure-credentials.sh` | Sets up secure credentials | `./scripts/setup/setup-secure-credentials.sh` |
|
||||
|
||||
## Build Scripts
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `scripts/build/build-claude-container.sh` | Builds the Claude container | `./scripts/build/build-claude-container.sh` |
|
||||
| `scripts/build/build-claudecode.sh` | Builds the Claude Code runner Docker image | `./scripts/build/build-claudecode.sh` |
|
||||
| `scripts/build/update-production-image.sh` | Updates the production Docker image | `./scripts/build/update-production-image.sh` |
|
||||
| `scripts/build/build.sh` | Builds the Docker images | `./scripts/build/build.sh` |
|
||||
|
||||
## AWS Configuration and Credentials
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `scripts/aws/create-aws-profile.sh` | Creates AWS profiles programmatically | `./scripts/aws/create-aws-profile.sh <profile-name> <access-key-id> <secret-access-key> [region] [output-format]` |
|
||||
| `scripts/aws/migrate-aws-credentials.sh` | Migrates AWS credentials to profiles | `./scripts/aws/migrate-aws-credentials.sh` |
|
||||
| `scripts/aws/setup-aws-profiles.sh` | Sets up AWS profiles | `./scripts/aws/setup-aws-profiles.sh` |
|
||||
| `scripts/aws/update-aws-creds.sh` | Updates AWS credentials | `./scripts/aws/update-aws-creds.sh` |
|
||||
|
||||
## Runtime and Execution
|
||||
|
||||
@@ -45,58 +40,48 @@ This document provides an overview of the scripts in this repository, organized
|
||||
|--------|-------------|-------|
|
||||
| `scripts/security/init-firewall.sh` | Initializes firewall for containers | `./scripts/security/init-firewall.sh` |
|
||||
| `scripts/security/accept-permissions.sh` | Handles permission acceptance | `./scripts/security/accept-permissions.sh` |
|
||||
| `scripts/security/fix-credential-references.sh` | Fixes credential references | `./scripts/security/fix-credential-references.sh` |
|
||||
| `scripts/security/credential-audit.sh` | Audits code for credential leaks | `./scripts/security/credential-audit.sh` |
|
||||
|
||||
## Utility Scripts
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `scripts/utils/ensure-test-dirs.sh` | Ensures test directories exist | `./scripts/utils/ensure-test-dirs.sh` |
|
||||
| `scripts/utils/prepare-clean-repo.sh` | Prepares a clean repository | `./scripts/utils/prepare-clean-repo.sh` |
|
||||
| `scripts/utils/volume-test.sh` | Tests volume mounting | `./scripts/utils/volume-test.sh` |
|
||||
| `scripts/utils/setup-repository-labels.js` | Sets up GitHub repository labels | `node scripts/utils/setup-repository-labels.js owner/repo` |
|
||||
|
||||
## Testing Scripts
|
||||
## Testing
|
||||
|
||||
### Integration Tests
|
||||
All shell-based test scripts have been migrated to JavaScript E2E tests using Jest. Use the following npm commands:
|
||||
|
||||
| Script | Description | Usage |
|
||||
### JavaScript Test Files
|
||||
|
||||
**Note**: Shell-based test scripts have been migrated to JavaScript E2E tests using Jest. The following test files provide comprehensive testing:
|
||||
|
||||
| Test File | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `test/integration/test-full-flow.sh` | Tests the full workflow | `./test/integration/test-full-flow.sh` |
|
||||
| `test/integration/test-claudecode-docker.sh` | Tests Claude Code Docker setup | `./test/integration/test-claudecode-docker.sh` |
|
||||
| `test/e2e/scenarios/container-execution.test.js` | Tests container functionality | `npm run test:e2e` |
|
||||
| `test/e2e/scenarios/claude-integration.test.js` | Tests Claude integration | `npm run test:e2e` |
|
||||
| `test/e2e/scenarios/docker-execution.test.js` | Tests Docker execution | `npm run test:e2e` |
|
||||
| `test/e2e/scenarios/security-firewall.test.js` | Tests security and firewall | `npm run test:e2e` |
|
||||
|
||||
### AWS Tests
|
||||
### Running Tests
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `test/aws/test-aws-profile.sh` | Tests AWS profile configuration | `./test/aws/test-aws-profile.sh` |
|
||||
| `test/aws/test-aws-mount.sh` | Tests AWS mount functionality | `./test/aws/test-aws-mount.sh` |
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
### Container Tests
|
||||
# Run unit tests
|
||||
npm run test:unit
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `test/container/test-basic-container.sh` | Tests basic container functionality | `./test/container/test-basic-container.sh` |
|
||||
| `test/container/test-container-cleanup.sh` | Tests container cleanup | `./test/container/test-container-cleanup.sh` |
|
||||
| `test/container/test-container-privileged.sh` | Tests container privileged mode | `./test/container/test-container-privileged.sh` |
|
||||
# Run E2E tests
|
||||
npm run test:e2e
|
||||
|
||||
### Claude Tests
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `test/claude/test-claude-direct.sh` | Tests direct Claude integration | `./test/claude/test-claude-direct.sh` |
|
||||
| `test/claude/test-claude-no-firewall.sh` | Tests Claude without firewall | `./test/claude/test-claude-no-firewall.sh` |
|
||||
| `test/claude/test-claude-installation.sh` | Tests Claude installation | `./test/claude/test-claude-installation.sh` |
|
||||
| `test/claude/test-claude-version.sh` | Tests Claude version | `./test/claude/test-claude-version.sh` |
|
||||
| `test/claude/test-claude-response.sh` | Tests Claude response | `./test/claude/test-claude-response.sh` |
|
||||
| `test/claude/test-direct-claude.sh` | Tests direct Claude access | `./test/claude/test-direct-claude.sh` |
|
||||
|
||||
### Security Tests
|
||||
|
||||
| Script | Description | Usage |
|
||||
|--------|-------------|-------|
|
||||
| `test/security/test-firewall.sh` | Tests firewall configuration | `./test/security/test-firewall.sh` |
|
||||
| `test/security/test-with-auth.sh` | Tests with authentication | `./test/security/test-with-auth.sh` |
|
||||
| `test/security/test-github-token.sh` | Tests GitHub token | `./test/security/test-github-token.sh` |
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
@@ -109,6 +94,9 @@ This document provides an overview of the scripts in this repository, organized
|
||||
# Set up Claude authentication
|
||||
./scripts/setup/setup-claude-auth.sh
|
||||
|
||||
# Set up secure credentials
|
||||
./scripts/setup/setup-secure-credentials.sh
|
||||
|
||||
# Create AWS profile
|
||||
./scripts/aws/create-aws-profile.sh claude-webhook YOUR_ACCESS_KEY YOUR_SECRET_KEY
|
||||
```
|
||||
@@ -116,8 +104,8 @@ This document provides an overview of the scripts in this repository, organized
|
||||
### Building and Running
|
||||
|
||||
```bash
|
||||
# Build Claude Code container
|
||||
./scripts/build/build-claudecode.sh
|
||||
# Build Docker images
|
||||
./scripts/build/build.sh
|
||||
|
||||
# Start the API server
|
||||
./scripts/runtime/start-api.sh
|
||||
@@ -129,22 +117,18 @@ docker compose up -d
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run integration tests
|
||||
./test/integration/test-full-flow.sh
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run AWS tests
|
||||
./test/aws/test-aws-profile.sh
|
||||
# Run E2E tests specifically
|
||||
npm run test:e2e
|
||||
|
||||
# Run Claude tests
|
||||
./test/claude/test-claude-direct.sh
|
||||
# Run unit tests specifically
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
## Notes
|
||||
|
||||
For backward compatibility, wrapper scripts are provided in the root directory for the most commonly used scripts:
|
||||
|
||||
- `setup-claude-auth.sh` -> `scripts/setup/setup-claude-auth.sh`
|
||||
- `build-claudecode.sh` -> `scripts/build/build-claudecode.sh`
|
||||
- `start-api.sh` -> `scripts/runtime/start-api.sh`
|
||||
|
||||
These wrappers simply forward all arguments to the actual scripts in their new locations.
|
||||
- All shell-based test scripts have been migrated to JavaScript E2E tests for better maintainability and consistency.
|
||||
- The project uses npm scripts for most common operations. See `package.json` for available scripts.
|
||||
- Docker Compose is the recommended way to run the service in production.
|
||||
222
docs/claude-authentication-guide.md
Normal file
222
docs/claude-authentication-guide.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Claude Authentication Guide
|
||||
|
||||
This guide covers three authentication methods for using Claude with the webhook service.
|
||||
|
||||
## Authentication Methods Overview
|
||||
|
||||
| Method | Use Case | Setup Complexity |
|
||||
|--------|----------|------------------|
|
||||
| **Setup Container** | Personal development | Medium |
|
||||
| **ANTHROPIC_API_KEY** | Production environments | Low |
|
||||
| **AWS Bedrock** | Enterprise integration | High |
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Option 1: Setup Container (Personal Development)
|
||||
|
||||
Uses personal Claude Code subscription for authentication.
|
||||
|
||||
### Setup Process
|
||||
|
||||
#### 1. Run Interactive Authentication Setup
|
||||
```bash
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
```
|
||||
|
||||
#### 2. Authenticate in Container
|
||||
When the container starts:
|
||||
```bash
|
||||
# In the container shell:
|
||||
claude --dangerously-skip-permissions # Follow authentication flow
|
||||
exit # Save authentication state
|
||||
```
|
||||
|
||||
#### 3. Test Captured Authentication
|
||||
```bash
|
||||
./scripts/setup/test-claude-auth.sh
|
||||
```
|
||||
|
||||
#### 4. Use Captured Authentication
|
||||
```bash
|
||||
# Option A: Copy to your main Claude directory
|
||||
cp -r ${CLAUDE_HUB_DIR:-~/.claude-hub}/* ~/.claude/
|
||||
|
||||
# Option B: Mount in docker-compose
|
||||
# Update docker-compose.yml:
|
||||
# - ./${CLAUDE_HUB_DIR:-~/.claude-hub}:/home/node/.claude
|
||||
```
|
||||
|
||||
#### 5. Verify Setup
|
||||
```bash
|
||||
node cli/webhook-cli.js --repo "owner/repo" --command "Test authentication" --url "http://localhost:8082"
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
- **Tokens expire**: Re-run authentication setup when needed
|
||||
- **File permissions**: Ensure `.credentials.json` is readable by container user
|
||||
- **Mount issues**: Verify correct path in docker-compose volume mounts
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Option 2: ANTHROPIC_API_KEY (Production)
|
||||
|
||||
Direct API key authentication for production environments.
|
||||
|
||||
### Setup Process
|
||||
|
||||
#### 1. Get API Key
|
||||
1. Go to [Anthropic Console](https://console.anthropic.com/)
|
||||
2. Create a new API key
|
||||
3. Copy the key (starts with `sk-ant-`)
|
||||
|
||||
#### 2. Configure Environment
|
||||
```bash
|
||||
# Add to .env file
|
||||
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
|
||||
```
|
||||
|
||||
#### 3. Restart Service
|
||||
```bash
|
||||
docker compose restart webhook
|
||||
```
|
||||
|
||||
#### 4. Test
|
||||
```bash
|
||||
node cli/webhook-cli.js --repo "owner/repo" --command "Test API key authentication" --url "http://localhost:8082"
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
- **Key rotation**: Regularly rotate API keys
|
||||
- **Environment security**: Never commit keys to version control
|
||||
- **Usage monitoring**: Monitor API usage through Anthropic Console
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Option 3: AWS Bedrock (Enterprise)
|
||||
|
||||
AWS-integrated Claude access for enterprise deployments.
|
||||
|
||||
### Setup Process
|
||||
|
||||
#### 1. Configure AWS Credentials
|
||||
```bash
|
||||
# Option A: AWS Profile (Recommended)
|
||||
./scripts/aws/create-aws-profile.sh
|
||||
|
||||
# Option B: Environment Variables
|
||||
export AWS_ACCESS_KEY_ID=your_access_key
|
||||
export AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||
export AWS_REGION=us-east-1
|
||||
```
|
||||
|
||||
#### 2. Configure Bedrock Settings
|
||||
```bash
|
||||
# Add to .env file
|
||||
CLAUDE_CODE_USE_BEDROCK=1
|
||||
ANTHROPIC_MODEL=us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
AWS_REGION=us-east-1
|
||||
|
||||
# If using profiles
|
||||
USE_AWS_PROFILE=true
|
||||
AWS_PROFILE=claude-webhook
|
||||
```
|
||||
|
||||
#### 3. Verify Bedrock Access
|
||||
```bash
|
||||
aws bedrock list-foundation-models --region us-east-1
|
||||
```
|
||||
|
||||
#### 4. Restart Service
|
||||
```bash
|
||||
docker compose restart webhook
|
||||
```
|
||||
|
||||
#### 5. Test
|
||||
```bash
|
||||
node cli/webhook-cli.js --repo "owner/repo" --command "Test Bedrock authentication" --url "http://localhost:8082"
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
- **IAM policies**: Use minimal required permissions
|
||||
- **Regional selection**: Choose appropriate AWS region
|
||||
- **Access logging**: Enable CloudTrail for audit compliance
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Authentication Priority and Fallback
|
||||
|
||||
The system checks authentication methods in this order:
|
||||
|
||||
1. **ANTHROPIC_API_KEY** (highest priority)
|
||||
2. **Claude Interactive Authentication** (setup container)
|
||||
3. **AWS Bedrock** (if configured)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Method 1: Direct API Key
|
||||
ANTHROPIC_API_KEY=sk-ant-your-key
|
||||
|
||||
# Method 2: Claude Interactive (automatic if ~/.claude is mounted)
|
||||
# No environment variables needed
|
||||
|
||||
# Method 3: AWS Bedrock
|
||||
CLAUDE_CODE_USE_BEDROCK=1
|
||||
ANTHROPIC_MODEL=us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=your_key_id
|
||||
AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||
# OR
|
||||
USE_AWS_PROFILE=true
|
||||
AWS_PROFILE=your-profile-name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Switching Between Methods
|
||||
|
||||
You can switch between authentication methods by updating your `.env` file:
|
||||
|
||||
```bash
|
||||
# Development with personal subscription
|
||||
# Comment out API key, ensure ~/.claude is mounted
|
||||
# ANTHROPIC_API_KEY=
|
||||
# Mount: ~/.claude:/home/node/.claude
|
||||
|
||||
# Production with API key
|
||||
ANTHROPIC_API_KEY=sk-ant-your-production-key
|
||||
|
||||
# Enterprise with Bedrock
|
||||
CLAUDE_CODE_USE_BEDROCK=1
|
||||
ANTHROPIC_MODEL=us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
USE_AWS_PROFILE=true
|
||||
AWS_PROFILE=production-claude
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Authentication Not Working
|
||||
1. Check environment variables are set correctly
|
||||
2. Verify API keys are valid and not expired
|
||||
3. For Bedrock: Ensure AWS credentials have correct permissions
|
||||
4. For setup container: Re-run authentication if tokens expired
|
||||
|
||||
### Rate Limiting
|
||||
- **API Key**: Contact Anthropic for rate limit information
|
||||
- **Bedrock**: Configure AWS throttling settings
|
||||
- **Setup Container**: Limited by subscription tier
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [Anthropic Console](https://console.anthropic.com/) - API key management
|
||||
- [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/) - Enterprise setup
|
||||
- [Claude Code Documentation](https://docs.anthropic.com/en/docs/claude-code) - Official Claude CLI docs
|
||||
- [Setup Container Deep Dive](./setup-container-guide.md) - Detailed setup container documentation
|
||||
|
||||
---
|
||||
|
||||
*This guide covers all authentication methods for the Claude GitHub Webhook service. Choose the method that best fits your technical requirements.*
|
||||
@@ -15,7 +15,7 @@ GitHub → Webhook Service → Docker Container → Claude API
|
||||
### 1. GitHub Webhook Reception
|
||||
|
||||
**Endpoint**: `POST /api/webhooks/github`
|
||||
**Handler**: `src/index.js:38`
|
||||
**Handler**: `src/index.ts:38`
|
||||
|
||||
1. GitHub sends webhook event to the service
|
||||
2. Express middleware captures raw body for signature verification
|
||||
@@ -23,7 +23,7 @@ GitHub → Webhook Service → Docker Container → Claude API
|
||||
|
||||
### 2. Webhook Verification & Processing
|
||||
|
||||
**Controller**: `src/controllers/githubController.js`
|
||||
**Controller**: `src/controllers/githubController.ts`
|
||||
**Method**: `handleWebhook()`
|
||||
|
||||
1. Verifies webhook signature using `GITHUB_WEBHOOK_SECRET`
|
||||
@@ -45,7 +45,7 @@ GitHub → Webhook Service → Docker Container → Claude API
|
||||
|
||||
### 4. Claude Container Preparation
|
||||
|
||||
**Service**: `src/services/claudeService.js`
|
||||
**Service**: `src/services/claudeService.ts`
|
||||
**Method**: `processCommand()`
|
||||
|
||||
1. Builds Docker image if not exists: `claude-code-runner:latest`
|
||||
@@ -79,7 +79,7 @@ GitHub → Webhook Service → Docker Container → Claude API
|
||||
|
||||
### 6. Response Handling
|
||||
|
||||
**Controller**: `src/controllers/githubController.js`
|
||||
**Controller**: `src/controllers/githubController.ts`
|
||||
**Method**: `handleWebhook()`
|
||||
|
||||
1. Read response from container
|
||||
|
||||
@@ -58,8 +58,8 @@ Instead of complex pooled execution, consider:
|
||||
|
||||
## Code Locations
|
||||
|
||||
- Container pool service: `src/services/containerPoolService.js`
|
||||
- Execution logic: `src/services/claudeService.js:170-210`
|
||||
- Container pool service: `src/services/containerPoolService.ts`
|
||||
- Execution logic: `src/services/claudeService.ts:170-210`
|
||||
- Container creation: Modified Docker command in pool service
|
||||
|
||||
## Performance Gains Observed
|
||||
|
||||
@@ -12,7 +12,7 @@ The webhook service handles sensitive credentials including:
|
||||
## Security Measures Implemented
|
||||
|
||||
### 1. Docker Command Sanitization
|
||||
In `src/services/claudeService.js`:
|
||||
In `src/services/claudeService.ts`:
|
||||
- Docker commands are sanitized before logging
|
||||
- Sensitive environment variables are replaced with `[REDACTED]`
|
||||
- Sanitized commands are used in all error messages
|
||||
@@ -34,13 +34,13 @@ const sanitizedCommand = dockerCommand.replace(/-e [A-Z_]+=\"[^\"]*\"/g, (match)
|
||||
- Sanitized output is used in error messages and logs
|
||||
|
||||
### 3. Logger Redaction
|
||||
In `src/utils/logger.js`:
|
||||
In `src/utils/logger.ts`:
|
||||
- Pino logger configured with comprehensive redaction paths
|
||||
- Automatically redacts sensitive fields in log output
|
||||
- Covers nested objects and various field patterns
|
||||
|
||||
### 4. Error Response Sanitization
|
||||
In `src/controllers/githubController.js`:
|
||||
In `src/controllers/githubController.ts`:
|
||||
- Only error messages (not full stack traces) are sent to GitHub
|
||||
- No raw stderr/stdout is exposed in webhook responses
|
||||
- Generic error messages for internal server errors
|
||||
|
||||
230
docs/docker-optimization.md
Normal file
230
docs/docker-optimization.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Docker Build Optimization Guide
|
||||
|
||||
This document describes the optimizations implemented in our Docker CI/CD pipeline for faster builds and better caching.
|
||||
|
||||
## Overview
|
||||
|
||||
Our optimized Docker build pipeline includes:
|
||||
- Self-hosted runner support with automatic fallback
|
||||
- Multi-stage builds for efficient layering
|
||||
- Advanced caching strategies
|
||||
- Container-based testing
|
||||
- Parallel builds for multiple images
|
||||
- Security scanning integration
|
||||
|
||||
## Self-Hosted Runners
|
||||
|
||||
### Configuration
|
||||
- **Labels**: `self-hosted, linux, x64, docker`
|
||||
- **Usage**: All Docker builds use self-hosted runners by default for improved performance
|
||||
- **Local Cache**: Self-hosted runners maintain Docker layer cache between builds
|
||||
- **Fallback**: Configurable via `USE_SELF_HOSTED` repository variable
|
||||
|
||||
### Runner Setup
|
||||
Self-hosted runners provide:
|
||||
- Persistent Docker layer cache
|
||||
- Faster builds (no image pull overhead)
|
||||
- Better network throughput for pushing images
|
||||
- Cost savings on GitHub Actions minutes
|
||||
|
||||
### Fallback Strategy
|
||||
The workflow implements a flexible fallback mechanism:
|
||||
|
||||
1. **Default behavior**: Uses self-hosted runners (`self-hosted, linux, x64, docker`)
|
||||
2. **Override option**: Set repository variable `USE_SELF_HOSTED=false` to force GitHub-hosted runners
|
||||
3. **Timeout protection**: 30-minute timeout prevents hanging on unavailable runners
|
||||
4. **Failure detection**: `build-fallback` job provides instructions if self-hosted runners fail
|
||||
|
||||
To manually switch to GitHub-hosted runners:
|
||||
```bash
|
||||
# Via GitHub UI: Settings → Secrets and variables → Actions → Variables
|
||||
# Add: USE_SELF_HOSTED = false
|
||||
|
||||
# Or via GitHub CLI:
|
||||
gh variable set USE_SELF_HOSTED --body "false"
|
||||
```
|
||||
|
||||
The runner selection logic:
|
||||
```yaml
|
||||
runs-on: ${{ fromJSON(format('["{0}"]', (vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || 'self-hosted, linux, x64, docker'))) }}
|
||||
```
|
||||
|
||||
## Multi-Stage Dockerfile
|
||||
|
||||
Our Dockerfile uses multiple stages for optimal caching and smaller images:
|
||||
|
||||
1. **Builder Stage**: Compiles TypeScript
|
||||
2. **Prod-deps Stage**: Installs production dependencies only
|
||||
3. **Test Stage**: Includes dev dependencies and test files
|
||||
4. **Production Stage**: Minimal runtime image
|
||||
|
||||
### Benefits
|
||||
- Parallel builds of independent stages
|
||||
- Smaller final image (no build tools or dev dependencies)
|
||||
- Test stage can run in CI without affecting production image
|
||||
- Better layer caching between builds
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### 1. GitHub Actions Cache (GHA)
|
||||
```yaml
|
||||
cache-from: type=gha,scope=${{ matrix.image }}-prod
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.image }}-prod
|
||||
```
|
||||
|
||||
### 2. Registry Cache
|
||||
```yaml
|
||||
cache-from: type=registry,ref=${{ org }}/claude-hub:nightly
|
||||
```
|
||||
|
||||
### 3. Inline Cache
|
||||
```yaml
|
||||
build-args: BUILDKIT_INLINE_CACHE=1
|
||||
outputs: type=inline
|
||||
```
|
||||
|
||||
### 4. Layer Ordering
|
||||
- Package files copied first (changes less frequently)
|
||||
- Source code copied after dependencies
|
||||
- Build artifacts cached between stages
|
||||
|
||||
## Container-Based Testing
|
||||
|
||||
Tests run inside Docker containers for:
|
||||
- Consistent environment
|
||||
- Parallel test execution
|
||||
- Isolation from host system
|
||||
- Same environment as production
|
||||
|
||||
### Test Execution
|
||||
```bash
|
||||
# Unit tests in container
|
||||
docker run --rm claude-hub:test npm test
|
||||
|
||||
# Integration tests with docker-compose
|
||||
docker-compose -f docker-compose.test.yml run integration-test
|
||||
|
||||
# E2E tests against running services
|
||||
docker-compose -f docker-compose.test.yml run e2e-test
|
||||
```
|
||||
|
||||
## Build Performance Optimizations
|
||||
|
||||
### 1. BuildKit Features
|
||||
- `DOCKER_BUILDKIT=1` for improved performance
|
||||
- `--mount=type=cache` for package manager caches
|
||||
- Parallel stage execution
|
||||
|
||||
### 2. Docker Buildx
|
||||
- Multi-platform builds (amd64, arm64)
|
||||
- Advanced caching backends
|
||||
- Build-only stages that don't ship to production
|
||||
|
||||
### 3. Context Optimization
|
||||
- `.dockerignore` excludes unnecessary files
|
||||
- Minimal context sent to Docker daemon
|
||||
- Faster uploads and builds
|
||||
|
||||
### 4. Dependency Caching
|
||||
- Separate stage for production dependencies
|
||||
- npm ci with --omit=dev for smaller images
|
||||
- Cache mount for npm packages
|
||||
|
||||
## Workflow Features
|
||||
|
||||
### PR Builds
|
||||
- Build and test without publishing
|
||||
- Single platform (amd64) for speed
|
||||
- Container-based test execution
|
||||
- Security scanning with Trivy
|
||||
|
||||
### Main Branch Builds
|
||||
- Multi-platform builds (amd64, arm64)
|
||||
- Push to registry with :nightly tag
|
||||
- Update cache images
|
||||
- Full test suite execution
|
||||
|
||||
### Version Tag Builds
|
||||
- Semantic versioning tags
|
||||
- :latest tag update
|
||||
- Multi-platform support
|
||||
- Production-ready images
|
||||
|
||||
## Security Scanning
|
||||
|
||||
### Integrated Scanners
|
||||
1. **Trivy**: Vulnerability scanning for Docker images
|
||||
2. **Hadolint**: Dockerfile linting
|
||||
3. **npm audit**: Dependency vulnerability checks
|
||||
4. **SARIF uploads**: Results visible in GitHub Security tab
|
||||
|
||||
## Monitoring and Metrics
|
||||
|
||||
### Build Performance
|
||||
- Build time per stage
|
||||
- Cache hit rates
|
||||
- Image size tracking
|
||||
- Test execution time
|
||||
|
||||
### Health Checks
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
### Building locally
|
||||
```bash
|
||||
# Build with BuildKit
|
||||
DOCKER_BUILDKIT=1 docker build -t claude-hub:local .
|
||||
|
||||
# Build specific stage
|
||||
docker build --target test -t claude-hub:test .
|
||||
|
||||
# Run tests locally
|
||||
docker-compose -f docker-compose.test.yml run test
|
||||
```
|
||||
|
||||
### Cache Management
|
||||
```bash
|
||||
# Clear builder cache
|
||||
docker builder prune
|
||||
|
||||
# Use local cache
|
||||
docker build --cache-from claude-hub:local .
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Order Dockerfile commands** from least to most frequently changing
|
||||
2. **Use specific versions** for base images and dependencies
|
||||
3. **Minimize layers** by combining RUN commands
|
||||
4. **Clean up** package manager caches in the same layer
|
||||
5. **Use multi-stage builds** to reduce final image size
|
||||
6. **Leverage BuildKit** features for better performance
|
||||
7. **Test in containers** for consistency across environments
|
||||
8. **Monitor build times** and optimize bottlenecks
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Slow Builds
|
||||
- Check cache hit rates in build logs
|
||||
- Verify .dockerignore is excluding large files
|
||||
- Use `--progress=plain` to see detailed timings
|
||||
- Consider parallelizing independent stages
|
||||
|
||||
### Cache Misses
|
||||
- Ensure consistent base image versions
|
||||
- Check for unnecessary file changes triggering rebuilds
|
||||
- Use cache mounts for package managers
|
||||
- Verify registry cache is accessible
|
||||
|
||||
### Test Failures in Container
|
||||
- Check environment variable differences
|
||||
- Verify volume mounts are correct
|
||||
- Ensure test dependencies are in test stage
|
||||
- Check for hardcoded paths or ports
|
||||
204
docs/environment-variables.md
Normal file
204
docs/environment-variables.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Environment Variables Documentation
|
||||
|
||||
This document provides a comprehensive list of all environment variables used in the Claude GitHub Webhook project.
|
||||
|
||||
## Table of Contents
|
||||
- [Core Application Configuration](#core-application-configuration)
|
||||
- [Bot Configuration](#bot-configuration)
|
||||
- [GitHub Configuration](#github-configuration)
|
||||
- [Claude/Anthropic Configuration](#claudeanthropic-configuration)
|
||||
- [Container Configuration](#container-configuration)
|
||||
- [AWS Configuration](#aws-configuration)
|
||||
- [PR Review Configuration](#pr-review-configuration)
|
||||
- [Security & Secrets Configuration](#security--secrets-configuration)
|
||||
- [Rate Limiting Configuration](#rate-limiting-configuration)
|
||||
- [Health Check Configuration](#health-check-configuration)
|
||||
- [Development/Test Variables](#developmenttest-variables)
|
||||
- [Shell Script Variables](#shell-script-variables)
|
||||
- [Hard-coded Values That Could Be Configurable](#hard-coded-values-that-could-be-configurable)
|
||||
|
||||
## Core Application Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `NODE_ENV` | Application environment (development/production/test) | `development` | No |
|
||||
| `PORT` | Server port | `3002` | No |
|
||||
| `TRUST_PROXY` | Trust proxy headers for X-Forwarded-For | `false` | No |
|
||||
|
||||
## Bot Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `BOT_USERNAME` | GitHub username the bot responds to (e.g., @ClaudeBot) | - | Yes |
|
||||
| `BOT_EMAIL` | Email used for git commits by the bot | - | Yes |
|
||||
| `DEFAULT_AUTHORIZED_USER` | Default authorized GitHub username | - | No |
|
||||
| `AUTHORIZED_USERS` | Comma-separated list of authorized GitHub usernames | - | No |
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `GITHUB_TOKEN` | GitHub personal access token | - | Yes |
|
||||
| `GITHUB_WEBHOOK_SECRET` | Secret for validating GitHub webhook payloads | - | Yes |
|
||||
| `DEFAULT_GITHUB_OWNER` | Default GitHub organization/owner | - | No |
|
||||
| `DEFAULT_GITHUB_USER` | Default GitHub username | - | No |
|
||||
| `DEFAULT_BRANCH` | Default git branch | `main` | No |
|
||||
| `TEST_REPO_FULL_NAME` | Test repository in owner/repo format | - | No |
|
||||
|
||||
## Claude/Anthropic Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `ANTHROPIC_API_KEY` | Anthropic API key for Claude access | - | Yes* |
|
||||
| `ANTHROPIC_MODEL` | Model name | `us.anthropic.claude-3-7-sonnet-20250219-v1:0` | No |
|
||||
| `CLAUDE_CODE_USE_BEDROCK` | Whether to use AWS Bedrock for Claude (0/1) | `0` | No |
|
||||
| `CLAUDE_HUB_DIR` | Directory for Claude Hub config | `~/.claude-hub` | No |
|
||||
| `CLAUDE_AUTH_HOST_DIR` | Host directory for Claude authentication | - | No |
|
||||
|
||||
*Required unless using AWS Bedrock or setup container authentication
|
||||
|
||||
## Container Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `CLAUDE_USE_CONTAINERS` | Enable container execution (0/1) | `1` | No |
|
||||
| `CLAUDE_CONTAINER_IMAGE` | Docker image for Claude containers | `claudecode:latest` | No |
|
||||
| `CLAUDE_CONTAINER_PRIVILEGED` | Run containers in privileged mode | `false` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_NET_RAW` | Add NET_RAW capability | `true` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_SYS_TIME` | Add SYS_TIME capability | `false` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_DAC_OVERRIDE` | Add DAC_OVERRIDE capability | `true` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_AUDIT_WRITE` | Add AUDIT_WRITE capability | `true` | No |
|
||||
| `CLAUDE_CONTAINER_CPU_SHARES` | CPU shares for containers | `1024` | No |
|
||||
| `CLAUDE_CONTAINER_MEMORY_LIMIT` | Memory limit for containers | `2g` | No |
|
||||
| `CLAUDE_CONTAINER_PIDS_LIMIT` | Process limit for containers | `256` | No |
|
||||
| `CONTAINER_LIFETIME_MS` | Container execution timeout in milliseconds | `7200000` (2 hours) | No |
|
||||
| `REPO_CACHE_DIR` | Directory for repository cache | `/tmp/repo-cache` | No |
|
||||
| `REPO_CACHE_MAX_AGE_MS` | Max age for cached repos in milliseconds | `3600000` (1 hour) | No |
|
||||
|
||||
## Claude Code Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `BASH_DEFAULT_TIMEOUT_MS` | Default timeout for bash commands in Claude Code | `600000` (10 minutes) | No |
|
||||
| `BASH_MAX_TIMEOUT_MS` | Maximum timeout Claude can set for bash commands | `1200000` (20 minutes) | No |
|
||||
|
||||
## AWS Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `AWS_ACCESS_KEY_ID` | AWS access key ID | - | No* |
|
||||
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key | - | No* |
|
||||
| `AWS_SESSION_TOKEN` | AWS session token (for temporary credentials) | - | No |
|
||||
| `AWS_SECURITY_TOKEN` | Alternative name for session token | - | No |
|
||||
| `AWS_REGION` | AWS region | `us-east-1` | No |
|
||||
| `AWS_PROFILE` | AWS profile name | - | No |
|
||||
| `USE_AWS_PROFILE` | Use AWS profile instead of direct credentials | `false` | No |
|
||||
| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` | ECS container credentials URI | - | No |
|
||||
|
||||
*Required if using AWS Bedrock for Claude
|
||||
|
||||
## PR Review Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `PR_REVIEW_WAIT_FOR_ALL_CHECKS` | Wait for all checks before PR review | `true` | No |
|
||||
| `PR_REVIEW_TRIGGER_WORKFLOW` | Specific workflow name to trigger PR review | - | No |
|
||||
| `PR_REVIEW_DEBOUNCE_MS` | Delay before checking all check suites | `5000` | No |
|
||||
| `PR_REVIEW_MAX_WAIT_MS` | Max wait for in-progress checks | `1800000` (30 min) | No |
|
||||
| `PR_REVIEW_CONDITIONAL_TIMEOUT_MS` | Timeout for conditional jobs | `300000` (5 min) | No |
|
||||
|
||||
## Security & Secrets Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `GITHUB_TOKEN_FILE` | Path to file containing GitHub token | `/run/secrets/github_token` | No |
|
||||
| `ANTHROPIC_API_KEY_FILE` | Path to file containing Anthropic API key | `/run/secrets/anthropic_api_key` | No |
|
||||
| `GITHUB_WEBHOOK_SECRET_FILE` | Path to file containing webhook secret | `/run/secrets/webhook_secret` | No |
|
||||
| `DISABLE_LOG_REDACTION` | Disable credential redaction in logs | `false` | No |
|
||||
|
||||
## Rate Limiting Configuration
|
||||
|
||||
These values are currently hard-coded but could be made configurable:
|
||||
|
||||
| Value | Description | Current Value | Location |
|
||||
|-------|-------------|---------------|----------|
|
||||
| Rate limit window | API rate limit time window | 15 minutes | `src/index.ts:32` |
|
||||
| Rate limit max requests | Max API requests per window | 100 | `src/index.ts:41` |
|
||||
| Webhook rate limit window | Webhook rate limit time window | 5 minutes | `src/index.ts:50` |
|
||||
| Webhook rate limit max requests | Max webhook requests per window | 50 | `src/index.ts:51` |
|
||||
|
||||
## Health Check Configuration
|
||||
|
||||
These values are defined in docker-compose.yml:
|
||||
|
||||
| Value | Description | Current Value |
|
||||
|-------|-------------|---------------|
|
||||
| Health check interval | Time between health checks | 30s |
|
||||
| Health check timeout | Timeout for each health check | 10s |
|
||||
| Health check retries | Number of retries before unhealthy | 3 |
|
||||
| Health check start period | Grace period on startup | 10s |
|
||||
|
||||
## Development/Test Variables
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `API_URL` | API URL for testing | `http://localhost:3003` | No |
|
||||
| `WEBHOOK_URL` | Webhook URL for testing | - | No |
|
||||
| `CLAUDE_API_AUTH_REQUIRED` | Require auth for Claude API | `false` | No |
|
||||
| `CLAUDE_API_AUTH_TOKEN` | Auth token for Claude API | - | No |
|
||||
| `HOME` | User home directory | - | No |
|
||||
| `WORKSPACE_PATH` | GitHub Actions workspace path | - | No |
|
||||
| `GITHUB_WORKSPACE` | GitHub Actions workspace | - | No |
|
||||
|
||||
## Shell Script Variables
|
||||
|
||||
| Variable | Description | Used In |
|
||||
|----------|-------------|---------|
|
||||
| `ALLOWED_TOOLS` | Tools allowed for Claude execution | entrypoint scripts |
|
||||
| `OPERATION_TYPE` | Type of operation (tagging, review, etc.) | entrypoint scripts |
|
||||
| `PRODUCTION_BOT` | Production bot username | setup scripts |
|
||||
| `STAGING_BOT` | Staging bot username | setup scripts |
|
||||
| `RUNNER_TOKEN` | GitHub Actions runner token | runner scripts |
|
||||
|
||||
## Hard-coded Values That Could Be Configurable
|
||||
|
||||
The following values are currently hard-coded in the source code but could potentially be made configurable via environment variables:
|
||||
|
||||
### Buffer Sizes
|
||||
- Docker execution buffer: 10MB (`src/services/claudeService.ts:160`)
|
||||
- Container logs buffer: 1MB (`src/services/claudeService.ts:184,590`)
|
||||
|
||||
### External URLs
|
||||
- EC2 metadata endpoint: `http://169.254.169.254/latest/meta-data/` (`src/utils/awsCredentialProvider.ts:94`)
|
||||
- GitHub API meta: `https://api.github.com/meta` (`scripts/security/init-firewall.sh:32`)
|
||||
|
||||
### Allowed Domains (Firewall)
|
||||
- `registry.npmjs.org`
|
||||
- `api.anthropic.com`
|
||||
- `sentry.io`
|
||||
- `statsig.anthropic.com`
|
||||
- `statsig.com`
|
||||
|
||||
### Default Values
|
||||
- Default git email in containers: `claude@example.com` (`scripts/runtime/claudecode-entrypoint.sh:89`)
|
||||
- Default git username in containers: `ClaudeBot` (`scripts/runtime/claudecode-entrypoint.sh:90`)
|
||||
- Health check container image: `claude-code-runner:latest` (`src/index.ts:140`)
|
||||
|
||||
### Docker Base Images
|
||||
- Node base image: `node:24` (`Dockerfile.claudecode:1`)
|
||||
- Delta version: `0.18.2` (`Dockerfile.claudecode:87`)
|
||||
- Zsh-in-docker version: `v1.2.0` (`Dockerfile.claudecode:91`)
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Secret Files**: The application supports loading secrets from files, which takes priority over environment variables. This is more secure for production deployments.
|
||||
|
||||
2. **AWS Authentication**: The service supports multiple AWS authentication methods:
|
||||
- Direct credentials (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)
|
||||
- AWS profiles (AWS_PROFILE with USE_AWS_PROFILE=true)
|
||||
- Instance profiles (EC2)
|
||||
- Task roles (ECS)
|
||||
|
||||
3. **Container Capabilities**: The container capability flags allow fine-grained control over container permissions for security purposes.
|
||||
|
||||
4. **Staging Environment**: Additional environment variables are defined in `.env.staging` for staging deployments, following the pattern `VARIABLE_NAME_STAGING`.
|
||||
275
docs/logging-security.md
Normal file
275
docs/logging-security.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Logging Security and Credential Redaction
|
||||
|
||||
This document describes the comprehensive credential redaction system implemented in the Claude GitHub Webhook service to prevent sensitive information from being exposed in logs.
|
||||
|
||||
## Overview
|
||||
|
||||
The logging system uses [Pino](https://getpino.io/) with comprehensive redaction patterns to automatically remove sensitive information from all log outputs. This ensures that credentials, secrets, tokens, and other sensitive data are never exposed in log files, console output, or external monitoring systems.
|
||||
|
||||
## Redaction Coverage
|
||||
|
||||
### Credential Types Protected
|
||||
|
||||
#### 1. AWS Credentials
|
||||
- **AWS_SECRET_ACCESS_KEY** - AWS secret access keys
|
||||
- **AWS_ACCESS_KEY_ID** - AWS access key identifiers (AKIA* pattern)
|
||||
- **AWS_SESSION_TOKEN** - Temporary session tokens
|
||||
- **AWS_SECURITY_TOKEN** - Security tokens
|
||||
|
||||
#### 2. GitHub Credentials
|
||||
- **GITHUB_TOKEN** - GitHub personal access tokens (ghp_* pattern)
|
||||
- **GH_TOKEN** - Alternative GitHub token environment variable
|
||||
- **GitHub PAT tokens** - Fine-grained personal access tokens (github_pat_* pattern)
|
||||
- **GITHUB_WEBHOOK_SECRET** - Webhook signature secrets
|
||||
|
||||
#### 3. Anthropic API Keys
|
||||
- **ANTHROPIC_API_KEY** - Claude API keys (sk-ant-* pattern)
|
||||
|
||||
#### 4. Database Credentials
|
||||
- **DATABASE_URL** - Full database connection strings
|
||||
- **DB_PASSWORD** - Database passwords
|
||||
- **REDIS_PASSWORD** - Redis authentication passwords
|
||||
- **connectionString** - SQL Server connection strings
|
||||
- **mongoUrl** - MongoDB connection URLs
|
||||
- **redisUrl** - Redis connection URLs
|
||||
|
||||
#### 5. Generic Sensitive Patterns
|
||||
- **password**, **passwd**, **pass** - Any password fields
|
||||
- **secret**, **secretKey**, **secret_key** - Any secret fields
|
||||
- **token** - Any token fields
|
||||
- **apiKey**, **api_key** - API key fields
|
||||
- **credential**, **credentials** - Credential fields
|
||||
- **key** - Generic key fields
|
||||
- **privateKey**, **private_key** - Private key content
|
||||
- **auth**, **authentication** - Authentication objects
|
||||
|
||||
#### 6. JWT and Token Types
|
||||
- **JWT_SECRET** - JWT signing secrets
|
||||
- **ACCESS_TOKEN** - OAuth access tokens
|
||||
- **REFRESH_TOKEN** - OAuth refresh tokens
|
||||
- **BOT_TOKEN** - Bot authentication tokens
|
||||
- **API_KEY** - Generic API keys
|
||||
- **SECRET_KEY** - Generic secret keys
|
||||
|
||||
#### 7. HTTP Headers
|
||||
- **authorization** - Authorization headers
|
||||
- **x-api-key** - API key headers
|
||||
- **x-auth-token** - Authentication token headers
|
||||
- **x-github-token** - GitHub token headers
|
||||
- **bearer** - Bearer token headers
|
||||
|
||||
### Context Coverage
|
||||
|
||||
#### 1. Top-Level Fields
|
||||
All sensitive field names are redacted when they appear as direct properties of logged objects.
|
||||
|
||||
#### 2. Nested Objects (up to 4 levels deep)
|
||||
Sensitive patterns are caught in deeply nested object structures:
|
||||
- `object.nested.password`
|
||||
- `config.database.connectionString`
|
||||
- `application.config.api.secret`
|
||||
- `deeply.nested.auth.token`
|
||||
|
||||
#### 3. Environment Variable Containers
|
||||
- **envVars.*** - Environment variable objects
|
||||
- **env.*** - Environment configuration objects
|
||||
- **process.env.*** - Process environment variables (using bracket notation)
|
||||
|
||||
#### 4. Error Objects
|
||||
- **error.message** - Error messages that might contain leaked credentials
|
||||
- **error.stderr** - Standard error output
|
||||
- **error.stdout** - Standard output
|
||||
- **error.dockerCommand** - Docker commands with embedded secrets
|
||||
- **err.*** - Alternative error object structures
|
||||
|
||||
#### 5. Output Streams
|
||||
- **stderr** - Standard error output
|
||||
- **stdout** - Standard output
|
||||
- **output** - Command output
|
||||
- **logs** - Log content
|
||||
- **message** - Message content
|
||||
- **data** - Generic data fields
|
||||
|
||||
#### 6. Docker and Command Context
|
||||
- **dockerCommand** - Docker run commands with -e flags
|
||||
- **dockerArgs** - Docker argument arrays
|
||||
- **command** - Shell commands that might contain secrets
|
||||
|
||||
#### 7. HTTP Request/Response Objects
|
||||
- **request.headers.authorization**
|
||||
- **response.headers.authorization**
|
||||
- **req.headers.***
|
||||
- **res.headers.***
|
||||
|
||||
#### 8. File Paths
|
||||
- **credentialsPath** - Paths to credential files
|
||||
- **keyPath** - Paths to key files
|
||||
- **secretPath** - Paths to secret files
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Pino Redaction Configuration
|
||||
|
||||
The redaction is implemented using Pino's built-in `redact` feature with a comprehensive array of path patterns:
|
||||
|
||||
```javascript
|
||||
redact: {
|
||||
paths: [
|
||||
// Over 200+ specific patterns covering all scenarios
|
||||
'password',
|
||||
'*.password',
|
||||
'*.*.password',
|
||||
'*.*.*.password',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'*.AWS_SECRET_ACCESS_KEY',
|
||||
'envVars.AWS_SECRET_ACCESS_KEY',
|
||||
'["process.env.AWS_SECRET_ACCESS_KEY"]',
|
||||
// ... many more patterns
|
||||
],
|
||||
censor: '[REDACTED]'
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern Types
|
||||
|
||||
1. **Direct patterns**: `'password'` - matches top-level fields
|
||||
2. **Single wildcard**: `'*.password'` - matches one level deep
|
||||
3. **Multi-wildcard**: `'*.*.password'` - matches multiple levels deep
|
||||
4. **Bracket notation**: `'["process.env.GITHUB_TOKEN"]'` - handles special characters
|
||||
5. **Nested paths**: `'envVars.AWS_SECRET_ACCESS_KEY'` - specific nested paths
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Coverage
|
||||
|
||||
The system includes comprehensive tests to verify redaction effectiveness:
|
||||
|
||||
#### 1. Basic Redaction Test (`test-logger-redaction.js`)
|
||||
- Tests all major credential types
|
||||
- Verifies nested object redaction
|
||||
- Ensures safe data remains visible
|
||||
|
||||
#### 2. Comprehensive Test Suite (`test-logger-redaction-comprehensive.js`)
|
||||
- 17 different test scenarios
|
||||
- Tests deep nesting (4+ levels)
|
||||
- Tests mixed safe/sensitive data
|
||||
- Tests edge cases and complex structures
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run basic redaction test
|
||||
node test/test-logger-redaction.js
|
||||
|
||||
# Run comprehensive test suite
|
||||
node test/test-logger-redaction-comprehensive.js
|
||||
|
||||
# Run full test suite
|
||||
npm test
|
||||
```
|
||||
|
||||
### Validation Checklist
|
||||
|
||||
When reviewing logs, ensure:
|
||||
|
||||
✅ **Should be [REDACTED]:**
|
||||
- All passwords, tokens, secrets, API keys
|
||||
- AWS credentials and session tokens
|
||||
- GitHub tokens and webhook secrets
|
||||
- Database connection strings and passwords
|
||||
- Docker commands containing sensitive environment variables
|
||||
- Error messages containing leaked credentials
|
||||
- HTTP headers with authorization data
|
||||
|
||||
✅ **Should remain visible:**
|
||||
- Usernames, emails, repo names, URLs
|
||||
- Public configuration values
|
||||
- Non-sensitive debugging information
|
||||
- Timestamps, log levels, component names
|
||||
|
||||
## Security Benefits
|
||||
|
||||
### 1. Compliance
|
||||
- Prevents credential exposure in logs
|
||||
- Supports audit requirements
|
||||
- Enables safe log aggregation and monitoring
|
||||
|
||||
### 2. Development Safety
|
||||
- Developers can safely share logs for debugging
|
||||
- Reduces risk of accidental credential exposure
|
||||
- Enables comprehensive logging without security concerns
|
||||
|
||||
### 3. Production Security
|
||||
- Log monitoring systems don't receive sensitive data
|
||||
- External log services (CloudWatch, Datadog, etc.) are safe
|
||||
- Log files can be safely stored and rotated
|
||||
|
||||
### 4. Incident Response
|
||||
- Detailed logs available for debugging without credential exposure
|
||||
- Error correlation IDs help track issues without revealing secrets
|
||||
- Safe log sharing between team members
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Regular Testing
|
||||
- Run redaction tests after any logging changes
|
||||
- Verify new credential patterns are covered
|
||||
- Test with realistic data scenarios
|
||||
|
||||
### 2. Pattern Maintenance
|
||||
- Add new patterns when introducing new credential types
|
||||
- Review and update patterns periodically
|
||||
- Consider deep nesting levels for complex objects
|
||||
|
||||
### 3. Monitoring
|
||||
- Monitor logs for any credential leakage
|
||||
- Use tools to scan logs for patterns that might indicate leaked secrets
|
||||
- Review error logs regularly for potential exposure
|
||||
|
||||
### 4. Development Guidelines
|
||||
- Always use structured logging with the logger utility
|
||||
- Avoid concatenating sensitive data into log messages
|
||||
- Use specific log levels appropriately
|
||||
- Test logging in development with real-like data structures
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
The logger automatically redacts these environment variables when they appear in logs:
|
||||
- `GITHUB_TOKEN`
|
||||
- `ANTHROPIC_API_KEY`
|
||||
- `AWS_SECRET_ACCESS_KEY`
|
||||
- `AWS_ACCESS_KEY_ID`
|
||||
- `GITHUB_WEBHOOK_SECRET`
|
||||
- And many more...
|
||||
|
||||
### Log Levels
|
||||
- **info**: General application flow
|
||||
- **warn**: Potentially harmful situations
|
||||
- **error**: Error events with full context (sanitized)
|
||||
- **debug**: Detailed information for diagnosing problems
|
||||
|
||||
### File Rotation
|
||||
- Production logs are automatically rotated at 10MB
|
||||
- Keeps up to 5 backup files
|
||||
- All rotated logs maintain redaction
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### If credentials appear in logs:
|
||||
1. Identify the specific pattern that wasn't caught
|
||||
2. Add the new pattern to the redaction paths in `src/utils/logger.ts`
|
||||
3. Add a test case in the test files
|
||||
4. Run tests to verify the fix
|
||||
5. Deploy the updated configuration
|
||||
|
||||
### Common issues:
|
||||
- **Deep nesting**: Add more wildcard levels (`*.*.*.*.pattern`)
|
||||
- **Special characters**: Use bracket notation (`["field-with-dashes"]`)
|
||||
- **New credential types**: Add to all relevant categories (top-level, nested, env vars)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [AWS Authentication Best Practices](./aws-authentication-best-practices.md)
|
||||
- [Credential Security](./credential-security.md)
|
||||
- [Container Security](./container-limitations.md)
|
||||
223
docs/setup-container-guide.md
Normal file
223
docs/setup-container-guide.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Setup Container Authentication
|
||||
|
||||
The setup container method captures Claude CLI authentication state for use in automated environments by preserving OAuth tokens and session data.
|
||||
|
||||
## Overview
|
||||
|
||||
Claude CLI requires interactive authentication. This container approach captures the authentication state from an interactive session and makes it available for automated use.
|
||||
|
||||
**Prerequisites**: Requires active Claude Code subscription.
|
||||
|
||||
## How It Works
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Setup Container] --> B[Interactive Claude Login]
|
||||
B --> C[OAuth Authentication]
|
||||
C --> D[Capture Auth State]
|
||||
D --> E[Mount in Production]
|
||||
E --> F[Automated Claude Usage]
|
||||
```
|
||||
|
||||
### 1. Interactive Authentication
|
||||
- Clean container environment with Claude CLI installed
|
||||
- User runs `claude --dangerously-skip-permissions` and completes authentication
|
||||
- OAuth tokens and session data stored in `~/.claude`
|
||||
|
||||
### 2. State Capture
|
||||
- Complete `~/.claude` directory copied to persistent storage on container exit
|
||||
- Includes credentials, settings, project data, and session info
|
||||
- Preserves all authentication context
|
||||
|
||||
### 3. Production Mount
|
||||
- Captured authentication mounted in production containers
|
||||
- Working copy created for each execution to avoid state conflicts
|
||||
- OAuth tokens used automatically by Claude CLI
|
||||
|
||||
## Technical Benefits
|
||||
|
||||
- **OAuth Security**: Uses OAuth tokens instead of API keys in environment variables
|
||||
- **Session Persistence**: Maintains Claude CLI session state across executions
|
||||
- **Portable**: Authentication state works across different container environments
|
||||
- **Reusable**: One-time setup supports multiple deployments
|
||||
|
||||
## Files Captured
|
||||
|
||||
The setup container captures all essential Claude authentication files:
|
||||
|
||||
```bash
|
||||
~/.claude/
|
||||
├── .credentials.json # OAuth tokens (primary auth)
|
||||
├── settings.local.json # User preferences
|
||||
├── projects/ # Project history
|
||||
├── todos/ # Task management data
|
||||
├── statsig/ # Analytics and feature flags
|
||||
└── package.json # CLI dependencies
|
||||
```
|
||||
|
||||
### Critical File: .credentials.json
|
||||
```json
|
||||
{
|
||||
"claudeAiOauth": {
|
||||
"accessToken": "sk-ant-oat01-...",
|
||||
"refreshToken": "sk-ant-ort01-...",
|
||||
"expiresAt": 1748658860401,
|
||||
"scopes": ["user:inference", "user:profile"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Container Implementation
|
||||
|
||||
### Setup Container (`Dockerfile.claude-setup`)
|
||||
- Node.js environment with Claude CLI
|
||||
- Interactive shell for authentication
|
||||
- Signal handling for clean state capture
|
||||
- Automatic file copying on exit
|
||||
|
||||
### Entrypoint Scripts
|
||||
- **Authentication copying**: Comprehensive file transfer
|
||||
- **Permission handling**: Correct ownership for container user
|
||||
- **Debug output**: Detailed logging for troubleshooting
|
||||
|
||||
## Token Lifecycle and Management
|
||||
|
||||
### Token Expiration Timeline
|
||||
Claude OAuth tokens typically expire within **8-12 hours**:
|
||||
- **Access tokens**: Short-lived (8-12 hours)
|
||||
- **Refresh tokens**: Longer-lived but also expire
|
||||
- **Automatic refresh**: Claude CLI attempts to refresh when needed
|
||||
|
||||
### Refresh Token Behavior
|
||||
```json
|
||||
{
|
||||
"claudeAiOauth": {
|
||||
"accessToken": "sk-ant-oat01-...", // Short-lived
|
||||
"refreshToken": "sk-ant-ort01-...", // Used to get new access tokens
|
||||
"expiresAt": 1748658860401, // Timestamp when access token expires
|
||||
"scopes": ["user:inference", "user:profile"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Refresh Strategy
|
||||
The Claude CLI automatically attempts to refresh tokens when:
|
||||
- Access token is expired or near expiration
|
||||
- API calls return authentication errors
|
||||
- Session state indicates refresh is needed
|
||||
|
||||
However, refresh tokens themselves eventually expire, requiring **full re-authentication**.
|
||||
|
||||
### Maintenance Requirements
|
||||
|
||||
**Monitoring**
|
||||
- Check authentication health regularly
|
||||
- Monitor for expired token errors in logs
|
||||
|
||||
**Re-authentication**
|
||||
- Required when OAuth tokens expire
|
||||
- Test authentication validity after updates
|
||||
|
||||
### Current Limitations
|
||||
|
||||
- Token refresh requires manual intervention
|
||||
- No automated re-authentication when tokens expire
|
||||
- Manual monitoring required for authentication health
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multiple Environments
|
||||
```bash
|
||||
# Development
|
||||
./${CLAUDE_HUB_DIR:-~/.claude-hub} → ~/.claude/
|
||||
|
||||
# Staging
|
||||
./claude-auth-staging → staging container
|
||||
|
||||
# Testing
|
||||
./claude-auth-test → test container
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Protection
|
||||
- OAuth tokens are sensitive credentials
|
||||
- Store in secure, encrypted storage
|
||||
- Rotate regularly by re-authenticating
|
||||
|
||||
### Container Security
|
||||
- Mount authentication with appropriate permissions
|
||||
- Use minimal container privileges
|
||||
- Avoid logging sensitive data
|
||||
|
||||
### Network Security
|
||||
- HTTPS for all Claude API communication
|
||||
- Secure token transmission
|
||||
- Monitor for token abuse
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
### Health Checks
|
||||
```bash
|
||||
# Test authentication status
|
||||
./scripts/setup/test-claude-auth.sh
|
||||
|
||||
# Verify token validity
|
||||
docker run --rm -v "./${CLAUDE_HUB_DIR:-~/.claude-hub}:/home/node/.claude:ro" \
|
||||
claude-setup:latest claude --dangerously-skip-permissions
|
||||
```
|
||||
|
||||
### Refresh Workflow
|
||||
```bash
|
||||
# When authentication expires
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
# Update production environment
|
||||
cp -r ${CLAUDE_HUB_DIR:-~/.claude-hub}/* ~/.claude/
|
||||
docker compose restart webhook
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Empty .credentials.json
|
||||
**Symptom**: Authentication fails, file exists but is 0 bytes
|
||||
**Cause**: Interactive authentication wasn't completed
|
||||
**Solution**: Re-run setup container and complete authentication flow
|
||||
|
||||
#### 2. Permission Errors
|
||||
**Symptom**: "Permission denied" accessing .credentials.json
|
||||
**Cause**: File ownership mismatch in container
|
||||
**Solution**: Entrypoint scripts handle this automatically
|
||||
|
||||
#### 3. OAuth Token Expired
|
||||
**Symptom**: "Invalid API key" or authentication errors
|
||||
**Cause**: Tokens expired (natural expiration)
|
||||
**Solution**: Re-authenticate using setup container
|
||||
|
||||
#### 4. Mount Path Issues
|
||||
**Symptom**: Authentication files not found in container
|
||||
**Cause**: Incorrect volume mount in docker-compose
|
||||
**Solution**: Verify mount path matches captured auth directory
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check captured files
|
||||
ls -la ${CLAUDE_HUB_DIR:-~/.claude-hub}/
|
||||
|
||||
# Test authentication directly
|
||||
docker run --rm -v "$(pwd)/${CLAUDE_HUB_DIR:-~/.claude-hub}:/tmp/auth:ro" \
|
||||
--entrypoint="" claude-setup:latest \
|
||||
bash -c "cp -r /tmp/auth /home/node/.claude &&
|
||||
sudo -u node env HOME=/home/node \
|
||||
/usr/local/share/npm-global/bin/claude --dangerously-skip-permissions --print 'test'"
|
||||
|
||||
# Verify OAuth tokens
|
||||
cat ${CLAUDE_HUB_DIR:-~/.claude-hub}/.credentials.json | jq '.claudeAiOauth'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*The setup container approach provides a technical solution for capturing and reusing Claude CLI authentication in automated environments.*
|
||||
@@ -1,7 +1,11 @@
|
||||
const js = require('@eslint/js');
|
||||
const tseslint = require('@typescript-eslint/eslint-plugin');
|
||||
const tsparser = require('@typescript-eslint/parser');
|
||||
const prettierConfig = require('eslint-config-prettier');
|
||||
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
prettierConfig, // Disable all formatting rules that conflict with Prettier
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@@ -32,11 +36,7 @@ module.exports = [
|
||||
'no-console': 'warn',
|
||||
'no-debugger': 'error',
|
||||
|
||||
// Code style
|
||||
'indent': ['error', 2],
|
||||
'quotes': ['error', 'single'],
|
||||
'semi': ['error', 'always'],
|
||||
'comma-dangle': ['error', 'never'],
|
||||
// Removed all formatting rules - let Prettier handle them
|
||||
|
||||
// Best practices
|
||||
'eqeqeq': 'error',
|
||||
@@ -65,9 +65,50 @@ module.exports = [
|
||||
'no-buffer-constructor': 'error'
|
||||
}
|
||||
},
|
||||
// TypeScript files configuration
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'commonjs',
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint
|
||||
},
|
||||
rules: {
|
||||
// Disable base rules that are covered by TypeScript equivalents
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' }],
|
||||
|
||||
// TypeScript specific rules
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/no-inferrable-types': 'error',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'warn',
|
||||
|
||||
// Style rules
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }]
|
||||
}
|
||||
},
|
||||
// Test files (JavaScript)
|
||||
{
|
||||
files: ['test/**/*.js', '**/*.test.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
jest: 'readonly',
|
||||
describe: 'readonly',
|
||||
@@ -83,5 +124,35 @@ module.exports = [
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
// Test files (TypeScript)
|
||||
{
|
||||
files: ['test/**/*.ts', '**/*.test.ts'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'commonjs',
|
||||
project: './tsconfig.test.json'
|
||||
},
|
||||
globals: {
|
||||
jest: 'readonly',
|
||||
describe: 'readonly',
|
||||
test: 'readonly',
|
||||
it: 'readonly',
|
||||
expect: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
afterEach: 'readonly',
|
||||
beforeAll: 'readonly',
|
||||
afterAll: 'readonly'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off' // Allow any in tests for mocking
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1,17 +1,38 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
setupFiles: ['<rootDir>/test/setup.js'],
|
||||
testMatch: [
|
||||
'**/test/unit/**/*.test.js',
|
||||
'**/test/integration/**/*.test.js',
|
||||
'**/test/e2e/scenarios/**/*.test.js'
|
||||
'**/test/unit/**/*.test.{js,ts}',
|
||||
'**/test/integration/**/*.test.{js,ts}',
|
||||
'**/test/e2e/scenarios/**/*.test.{js,ts}'
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
'^.+\\.js$': 'babel-jest'
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(universal-user-agent|@octokit|before-after-hook)/)'
|
||||
],
|
||||
collectCoverage: true,
|
||||
coverageReporters: ['text', 'lcov'],
|
||||
coverageDirectory: 'coverage',
|
||||
coveragePathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/dist/',
|
||||
'/coverage/'
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,ts}',
|
||||
'!src/**/*.d.ts',
|
||||
'!**/node_modules/**',
|
||||
'!**/dist/**'
|
||||
],
|
||||
testTimeout: 30000, // Some tests might take longer due to container initialization
|
||||
verbose: true,
|
||||
reporters: [
|
||||
'default',
|
||||
['jest-junit', { outputDirectory: 'test-results/jest', outputName: 'results.xml' }]
|
||||
],
|
||||
]
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: claude-webhook-secrets
|
||||
namespace: default
|
||||
type: Opaque
|
||||
stringData:
|
||||
github-token: "YOUR_GITHUB_TOKEN_HERE"
|
||||
anthropic-api-key: "YOUR_ANTHROPIC_API_KEY_HERE"
|
||||
webhook-secret: "YOUR_WEBHOOK_SECRET_HERE"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: claude-webhook
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: claude-webhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: claude-webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
image: claude-webhook:latest
|
||||
ports:
|
||||
- containerPort: 3002
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "3002"
|
||||
- name: GITHUB_TOKEN_FILE
|
||||
value: "/etc/secrets/github-token"
|
||||
- name: ANTHROPIC_API_KEY_FILE
|
||||
value: "/etc/secrets/anthropic-api-key"
|
||||
- name: GITHUB_WEBHOOK_SECRET_FILE
|
||||
value: "/etc/secrets/webhook-secret"
|
||||
volumeMounts:
|
||||
- name: secrets-volume
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: secrets-volume
|
||||
secret:
|
||||
secretName: claude-webhook-secrets
|
||||
items:
|
||||
- key: github-token
|
||||
path: github-token
|
||||
- key: anthropic-api-key
|
||||
path: anthropic-api-key
|
||||
- key: webhook-secret
|
||||
path: webhook-secret
|
||||
3059
package-lock.json
generated
3059
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,18 +1,27 @@
|
||||
{
|
||||
"name": "claude-github-webhook",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.1",
|
||||
"description": "A webhook endpoint for Claude to perform git and GitHub actions",
|
||||
"main": "src/index.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"test": "jest",
|
||||
"test:unit": "jest --testMatch='**/test/unit/**/*.test.js'",
|
||||
"test:integration": "jest --testMatch='**/test/integration/**/*.test.js'",
|
||||
"test:e2e": "jest --testMatch='**/test/e2e/scenarios/**/*.test.js'",
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"start:dev": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"dev:watch": "nodemon --exec ts-node src/index.ts",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest --testPathPattern='test/(unit|integration).*\\.test\\.(js|ts)$'",
|
||||
"test:unit": "jest --testMatch='**/test/unit/**/*.test.{js,ts}'",
|
||||
"test:integration": "jest --testMatch='**/test/integration/**/*.test.{js,ts}'",
|
||||
"test:e2e": "jest --testMatch='**/test/e2e/**/*.test.{js,ts}'",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"test:ci": "jest --ci --coverage",
|
||||
"test:ci": "jest --ci --coverage --testPathPattern='test/(unit|integration).*\\.test\\.(js|ts)$'",
|
||||
"test:docker": "docker-compose -f docker-compose.test.yml run --rm test",
|
||||
"test:docker:integration": "docker-compose -f docker-compose.test.yml run --rm integration-test",
|
||||
"test:docker:e2e": "docker-compose -f docker-compose.test.yml run --rm e2e-test",
|
||||
"pretest": "./scripts/utils/ensure-test-dirs.sh",
|
||||
"lint": "eslint src/ test/ --fix",
|
||||
"lint:check": "eslint src/ test/",
|
||||
@@ -20,27 +29,47 @@
|
||||
"format:check": "prettier --check src/ test/",
|
||||
"security:audit": "npm audit --audit-level=moderate",
|
||||
"security:fix": "npm audit fix",
|
||||
"setup:dev": "husky install"
|
||||
"setup:dev": "husky install",
|
||||
"setup:hooks": "husky",
|
||||
"prepare": "husky || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"axios": "^1.6.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0"
|
||||
"pino-pretty": "^13.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.3",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@jest/globals": "^30.0.0-beta.3",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.23",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||
"@typescript-eslint/parser": "^8.33.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-node": "^4.1.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.0.0",
|
||||
"supertest": "^7.1.1"
|
||||
"supertest": "^7.1.1",
|
||||
"ts-jest": "^29.3.4",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker Hub publishing script for Claude GitHub Webhook
|
||||
# Usage: ./publish-docker.sh YOUR_DOCKERHUB_USERNAME [VERSION]
|
||||
|
||||
DOCKERHUB_USERNAME=${1:-intelligenceassist}
|
||||
VERSION=${2:-latest}
|
||||
|
||||
# Default to intelligenceassist organization
|
||||
|
||||
IMAGE_NAME="claude-github-webhook"
|
||||
FULL_IMAGE_NAME="$DOCKERHUB_USERNAME/$IMAGE_NAME"
|
||||
|
||||
echo "Building Docker image..."
|
||||
docker build -t $IMAGE_NAME:latest .
|
||||
|
||||
echo "Tagging image as $FULL_IMAGE_NAME:$VERSION..."
|
||||
docker tag $IMAGE_NAME:latest $FULL_IMAGE_NAME:$VERSION
|
||||
|
||||
if [ "$VERSION" != "latest" ]; then
|
||||
echo "Also tagging as $FULL_IMAGE_NAME:latest..."
|
||||
docker tag $IMAGE_NAME:latest $FULL_IMAGE_NAME:latest
|
||||
fi
|
||||
|
||||
echo "Logging in to Docker Hub..."
|
||||
docker login
|
||||
|
||||
echo "Pushing to Docker Hub..."
|
||||
docker push $FULL_IMAGE_NAME:$VERSION
|
||||
|
||||
if [ "$VERSION" != "latest" ]; then
|
||||
docker push $FULL_IMAGE_NAME:latest
|
||||
fi
|
||||
|
||||
echo "Successfully published to Docker Hub!"
|
||||
echo "Users can now pull with: docker pull $FULL_IMAGE_NAME:$VERSION"
|
||||
@@ -1,263 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Script to clean up redundant scripts after reorganization
|
||||
echo "Starting script cleanup..."
|
||||
|
||||
# Create a backup directory for redundant scripts
|
||||
BACKUP_DIR="./scripts/archived"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
echo "Created backup directory: $BACKUP_DIR"
|
||||
|
||||
# Function to archive a script instead of deleting it
|
||||
archive_script() {
|
||||
local script=$1
|
||||
if [ -f "$script" ]; then
|
||||
echo "Archiving $script to $BACKUP_DIR"
|
||||
git mv "$script" "$BACKUP_DIR/$(basename $script)"
|
||||
else
|
||||
echo "Warning: $script not found, skipping"
|
||||
fi
|
||||
}
|
||||
|
||||
# Archive redundant test scripts
|
||||
echo "Archiving redundant test scripts..."
|
||||
archive_script "test/claude/test-direct-claude.sh" # Duplicate of test-claude-direct.sh
|
||||
archive_script "test/claude/test-claude-version.sh" # Can be merged with test-claude-installation.sh
|
||||
|
||||
# Archive obsolete AWS credential scripts
|
||||
echo "Archiving obsolete AWS credential scripts..."
|
||||
archive_script "scripts/aws/update-aws-creds.sh" # Obsolete, replaced by profile-based auth
|
||||
|
||||
# Archive temporary/one-time setup scripts
|
||||
echo "Moving one-time setup scripts to archived directory..."
|
||||
mkdir -p "$BACKUP_DIR/one-time"
|
||||
git mv "scripts/utils/prepare-clean-repo.sh" "$BACKUP_DIR/one-time/"
|
||||
git mv "scripts/utils/fix-credential-references.sh" "$BACKUP_DIR/one-time/"
|
||||
|
||||
# Archive redundant container test scripts that can be consolidated
|
||||
echo "Archiving redundant container test scripts..."
|
||||
archive_script "test/container/test-container-privileged.sh" # Can be merged with test-basic-container.sh
|
||||
|
||||
# Archive our temporary reorganization scripts
|
||||
echo "Archiving temporary reorganization scripts..."
|
||||
git mv "reorganize-scripts.sh" "$BACKUP_DIR/one-time/"
|
||||
git mv "script-organization.md" "$BACKUP_DIR/one-time/"
|
||||
|
||||
# After archiving, create a consolidated container test script
|
||||
echo "Creating consolidated container test script..."
|
||||
cat > test/container/test-container.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Consolidated container test script
|
||||
# Usage: ./test-container.sh [basic|privileged|cleanup]
|
||||
|
||||
set -e
|
||||
|
||||
TEST_TYPE=${1:-basic}
|
||||
|
||||
case "$TEST_TYPE" in
|
||||
basic)
|
||||
echo "Running basic container test..."
|
||||
# Basic container test logic from test-basic-container.sh
|
||||
docker run --rm -it \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="echo 'Basic container test'" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
privileged)
|
||||
echo "Running privileged container test..."
|
||||
# Privileged container test logic from test-container-privileged.sh
|
||||
docker run --rm -it \
|
||||
--privileged \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="echo 'Privileged container test'" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
cleanup)
|
||||
echo "Running container cleanup test..."
|
||||
# Container cleanup test logic from test-container-cleanup.sh
|
||||
docker run --rm -it \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="echo 'Container cleanup test'" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown test type: $TEST_TYPE"
|
||||
echo "Usage: ./test-container.sh [basic|privileged|cleanup]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Test complete!"
|
||||
EOF
|
||||
chmod +x test/container/test-container.sh
|
||||
|
||||
# Create a consolidated Claude test script
|
||||
echo "Creating consolidated Claude test script..."
|
||||
cat > test/claude/test-claude.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Consolidated Claude test script
|
||||
# Usage: ./test-claude.sh [direct|installation|no-firewall|response]
|
||||
|
||||
set -e
|
||||
|
||||
TEST_TYPE=${1:-direct}
|
||||
|
||||
case "$TEST_TYPE" in
|
||||
direct)
|
||||
echo "Testing direct Claude integration..."
|
||||
# Direct Claude test logic from test-claude-direct.sh
|
||||
docker run --rm -it \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="echo 'Direct Claude test'" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
-e ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-test-key}" \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
installation)
|
||||
echo "Testing Claude installation..."
|
||||
# Installation test logic from test-claude-installation.sh and test-claude-version.sh
|
||||
docker run --rm -it \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="claude-cli --version && claude --version" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
no-firewall)
|
||||
echo "Testing Claude without firewall..."
|
||||
# Test logic from test-claude-no-firewall.sh
|
||||
docker run --rm -it \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="echo 'Claude without firewall test'" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
-e DISABLE_FIREWALL=true \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
response)
|
||||
echo "Testing Claude response..."
|
||||
# Test logic from test-claude-response.sh
|
||||
docker run --rm -it \
|
||||
-e REPO_FULL_NAME="owner/test-repo" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="claude \"Tell me a joke\"" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-test-token}" \
|
||||
-e ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-test-key}" \
|
||||
claude-code-runner:latest
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown test type: $TEST_TYPE"
|
||||
echo "Usage: ./test-claude.sh [direct|installation|no-firewall|response]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Test complete!"
|
||||
EOF
|
||||
chmod +x test/claude/test-claude.sh
|
||||
|
||||
# Create a consolidated build script
|
||||
echo "Creating consolidated build script..."
|
||||
cat > scripts/build/build.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Consolidated build script
|
||||
# Usage: ./build.sh [claude|claudecode|production]
|
||||
|
||||
set -e
|
||||
|
||||
BUILD_TYPE=${1:-claudecode}
|
||||
|
||||
case "$BUILD_TYPE" in
|
||||
claude)
|
||||
echo "Building Claude container..."
|
||||
docker build -f Dockerfile.claude -t claude-container:latest .
|
||||
;;
|
||||
|
||||
claudecode)
|
||||
echo "Building Claude Code runner Docker image..."
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:latest .
|
||||
;;
|
||||
|
||||
production)
|
||||
if [ ! -d "./claude-config" ]; then
|
||||
echo "Error: claude-config directory not found."
|
||||
echo "Please run ./scripts/setup/setup-claude-auth.sh first and copy the config."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building production image with pre-authenticated config..."
|
||||
cp Dockerfile.claudecode Dockerfile.claudecode.backup
|
||||
# Production build logic from update-production-image.sh
|
||||
# ... (truncated for brevity)
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:production .
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build type: $BUILD_TYPE"
|
||||
echo "Usage: ./build.sh [claude|claudecode|production]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Build complete!"
|
||||
EOF
|
||||
chmod +x scripts/build/build.sh
|
||||
|
||||
# Update documentation to reflect the changes
|
||||
echo "Updating documentation..."
|
||||
sed -i 's|test-direct-claude.sh|test-claude.sh direct|g' SCRIPTS.md
|
||||
sed -i 's|test-claude-direct.sh|test-claude.sh direct|g' SCRIPTS.md
|
||||
sed -i 's|test-claude-version.sh|test-claude.sh installation|g' SCRIPTS.md
|
||||
sed -i 's|test-claude-installation.sh|test-claude.sh installation|g' SCRIPTS.md
|
||||
sed -i 's|test-claude-no-firewall.sh|test-claude.sh no-firewall|g' SCRIPTS.md
|
||||
sed -i 's|test-claude-response.sh|test-claude.sh response|g' SCRIPTS.md
|
||||
|
||||
sed -i 's|test-basic-container.sh|test-container.sh basic|g' SCRIPTS.md
|
||||
sed -i 's|test-container-privileged.sh|test-container.sh privileged|g' SCRIPTS.md
|
||||
sed -i 's|test-container-cleanup.sh|test-container.sh cleanup|g' SCRIPTS.md
|
||||
|
||||
sed -i 's|build-claude-container.sh|build.sh claude|g' SCRIPTS.md
|
||||
sed -i 's|build-claudecode.sh|build.sh claudecode|g' SCRIPTS.md
|
||||
sed -i 's|update-production-image.sh|build.sh production|g' SCRIPTS.md
|
||||
|
||||
# Create a final wrapper script for backward compatibility
|
||||
cat > build-claudecode.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Wrapper script for backward compatibility
|
||||
echo "This script is now located at scripts/build/build.sh"
|
||||
exec scripts/build/build.sh claudecode "$@"
|
||||
EOF
|
||||
chmod +x build-claudecode.sh
|
||||
|
||||
# After all operations are complete, clean up this script too
|
||||
echo "Script cleanup complete!"
|
||||
echo
|
||||
echo "Note: This script (cleanup-scripts.sh) has completed its job and can now be removed."
|
||||
echo "After verifying the changes, you can remove it with:"
|
||||
echo "rm cleanup-scripts.sh"
|
||||
echo
|
||||
echo "To commit these changes, run:"
|
||||
echo "git add ."
|
||||
echo "git commit -m \"Clean up redundant scripts and consolidate functionality\""
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This script prepares a clean repository without sensitive files
|
||||
|
||||
# Set directories
|
||||
CURRENT_REPO="/home/jonflatt/n8n/claude-repo"
|
||||
CLEAN_REPO="/tmp/clean-repo"
|
||||
|
||||
# Create clean repo directory if it doesn't exist
|
||||
mkdir -p "$CLEAN_REPO"
|
||||
|
||||
# Files and patterns to exclude
|
||||
EXCLUDES=(
|
||||
".git"
|
||||
".env"
|
||||
".env.backup"
|
||||
"node_modules"
|
||||
"coverage"
|
||||
"\\"
|
||||
)
|
||||
|
||||
# Build rsync exclude arguments
|
||||
EXCLUDE_ARGS=""
|
||||
for pattern in "${EXCLUDES[@]}"; do
|
||||
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude='$pattern'"
|
||||
done
|
||||
|
||||
# Sync files to clean repo
|
||||
echo "Copying files to clean repository..."
|
||||
eval "rsync -av $EXCLUDE_ARGS $CURRENT_REPO/ $CLEAN_REPO/"
|
||||
|
||||
# Create a new .gitignore if it doesn't exist
|
||||
if [ ! -f "$CLEAN_REPO/.gitignore" ]; then
|
||||
echo "Creating .gitignore..."
|
||||
cat > "$CLEAN_REPO/.gitignore" << EOF
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.backup
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
|
||||
# Temp directory
|
||||
tmp/
|
||||
|
||||
# Test results
|
||||
test-results/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
/response.txt
|
||||
"\\"
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Clean repository prepared at $CLEAN_REPO"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Create a new GitHub repository"
|
||||
echo "2. Initialize the clean repository with git:"
|
||||
echo " cd $CLEAN_REPO"
|
||||
echo " git init"
|
||||
echo " git add ."
|
||||
echo " git commit -m \"Initial commit\""
|
||||
echo "3. Set the remote origin and push:"
|
||||
echo " git remote add origin <new-repository-url>"
|
||||
echo " git push -u origin main"
|
||||
echo ""
|
||||
echo "Important: Make sure to review the files once more before committing to ensure no sensitive data is included."
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Script to reorganize the script files according to the proposed structure
|
||||
echo "Starting script reorganization..."
|
||||
|
||||
# Create directory structure
|
||||
echo "Creating directory structure..."
|
||||
mkdir -p scripts/setup
|
||||
mkdir -p scripts/build
|
||||
mkdir -p scripts/aws
|
||||
mkdir -p scripts/runtime
|
||||
mkdir -p scripts/security
|
||||
mkdir -p scripts/utils
|
||||
|
||||
mkdir -p test/integration
|
||||
mkdir -p test/aws
|
||||
mkdir -p test/container
|
||||
mkdir -p test/claude
|
||||
mkdir -p test/security
|
||||
mkdir -p test/utils
|
||||
|
||||
# Move setup scripts
|
||||
echo "Moving setup scripts..."
|
||||
git mv scripts/setup.sh scripts/setup/
|
||||
git mv scripts/setup-precommit.sh scripts/setup/
|
||||
git mv setup-claude-auth.sh scripts/setup/
|
||||
git mv setup-new-repo.sh scripts/setup/
|
||||
git mv create-new-repo.sh scripts/setup/
|
||||
|
||||
# Move build scripts
|
||||
echo "Moving build scripts..."
|
||||
git mv build-claude-container.sh scripts/build/
|
||||
git mv build-claudecode.sh scripts/build/
|
||||
git mv update-production-image.sh scripts/build/
|
||||
|
||||
# Move AWS scripts
|
||||
echo "Moving AWS scripts..."
|
||||
git mv scripts/create-aws-profile.sh scripts/aws/
|
||||
git mv scripts/migrate-aws-credentials.sh scripts/aws/
|
||||
git mv scripts/setup-aws-profiles.sh scripts/aws/
|
||||
git mv update-aws-creds.sh scripts/aws/
|
||||
|
||||
# Move runtime scripts
|
||||
echo "Moving runtime scripts..."
|
||||
git mv start-api.sh scripts/runtime/
|
||||
git mv entrypoint.sh scripts/runtime/
|
||||
git mv claudecode-entrypoint.sh scripts/runtime/
|
||||
git mv startup.sh scripts/runtime/
|
||||
git mv claude-wrapper.sh scripts/runtime/
|
||||
|
||||
# Move security scripts
|
||||
echo "Moving security scripts..."
|
||||
git mv init-firewall.sh scripts/security/
|
||||
git mv accept-permissions.sh scripts/security/
|
||||
git mv fix-credential-references.sh scripts/security/
|
||||
|
||||
# Move utility scripts
|
||||
echo "Moving utility scripts..."
|
||||
git mv scripts/ensure-test-dirs.sh scripts/utils/
|
||||
git mv prepare-clean-repo.sh scripts/utils/
|
||||
git mv volume-test.sh scripts/utils/
|
||||
|
||||
# Move test scripts
|
||||
echo "Moving test scripts..."
|
||||
git mv test/test-full-flow.sh test/integration/
|
||||
git mv test/test-claudecode-docker.sh test/integration/
|
||||
|
||||
git mv test/test-aws-profile.sh test/aws/
|
||||
git mv test/test-aws-mount.sh test/aws/
|
||||
|
||||
git mv test/test-basic-container.sh test/container/
|
||||
git mv test/test-container-cleanup.sh test/container/
|
||||
git mv test/test-container-privileged.sh test/container/
|
||||
|
||||
git mv test/test-claude-direct.sh test/claude/
|
||||
git mv test/test-claude-no-firewall.sh test/claude/
|
||||
git mv test/test-claude-installation.sh test/claude/
|
||||
git mv test/test-claude-version.sh test/claude/
|
||||
git mv test/test-claude-response.sh test/claude/
|
||||
git mv test/test-direct-claude.sh test/claude/
|
||||
|
||||
git mv test/test-firewall.sh test/security/
|
||||
git mv test/test-with-auth.sh test/security/
|
||||
git mv test/test-github-token.sh test/security/
|
||||
|
||||
# Create wrapper scripts for backward compatibility
|
||||
echo "Creating wrapper scripts for backward compatibility..."
|
||||
|
||||
cat > setup-claude-auth.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Wrapper script for backward compatibility
|
||||
echo "This script is now located at scripts/setup/setup-claude-auth.sh"
|
||||
exec scripts/setup/setup-claude-auth.sh "$@"
|
||||
EOF
|
||||
chmod +x setup-claude-auth.sh
|
||||
|
||||
cat > build-claudecode.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Wrapper script for backward compatibility
|
||||
echo "This script is now located at scripts/build/build-claudecode.sh"
|
||||
exec scripts/build/build-claudecode.sh "$@"
|
||||
EOF
|
||||
chmod +x build-claudecode.sh
|
||||
|
||||
cat > start-api.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Wrapper script for backward compatibility
|
||||
echo "This script is now located at scripts/runtime/start-api.sh"
|
||||
exec scripts/runtime/start-api.sh "$@"
|
||||
EOF
|
||||
chmod +x start-api.sh
|
||||
|
||||
# Update docker-compose.yml file if it references specific script paths
|
||||
echo "Checking for docker-compose.yml updates..."
|
||||
if [ -f docker-compose.yml ]; then
|
||||
sed -i 's#./claudecode-entrypoint.sh#./scripts/runtime/claudecode-entrypoint.sh#g' docker-compose.yml
|
||||
sed -i 's#./entrypoint.sh#./scripts/runtime/entrypoint.sh#g' docker-compose.yml
|
||||
fi
|
||||
|
||||
# Update Dockerfile.claudecode if it references specific script paths
|
||||
echo "Checking for Dockerfile.claudecode updates..."
|
||||
if [ -f Dockerfile.claudecode ]; then
|
||||
sed -i 's#COPY init-firewall.sh#COPY scripts/security/init-firewall.sh#g' Dockerfile.claudecode
|
||||
sed -i 's#COPY claudecode-entrypoint.sh#COPY scripts/runtime/claudecode-entrypoint.sh#g' Dockerfile.claudecode
|
||||
fi
|
||||
|
||||
echo "Script reorganization complete!"
|
||||
echo
|
||||
echo "Please review the changes and test that all scripts still work properly."
|
||||
echo "You may need to update additional references in other files or scripts."
|
||||
echo
|
||||
echo "To commit these changes, run:"
|
||||
echo "git add ."
|
||||
echo "git commit -m \"Reorganize scripts into a more structured directory layout\""
|
||||
@@ -1,128 +0,0 @@
|
||||
# Script Organization Proposal
|
||||
|
||||
## Categories of Scripts
|
||||
|
||||
### 1. Setup and Installation
|
||||
- `scripts/setup.sh` - Main setup script for the project
|
||||
- `scripts/setup-precommit.sh` - Sets up pre-commit hooks
|
||||
- `setup-claude-auth.sh` - Sets up Claude authentication
|
||||
- `setup-new-repo.sh` - Sets up a new clean repository
|
||||
- `create-new-repo.sh` - Creates a new repository
|
||||
|
||||
### 2. Build Scripts
|
||||
- `build-claude-container.sh` - Builds the Claude container
|
||||
- `build-claudecode.sh` - Builds the Claude Code runner Docker image
|
||||
- `update-production-image.sh` - Updates the production Docker image
|
||||
|
||||
### 3. AWS Configuration and Credentials
|
||||
- `scripts/create-aws-profile.sh` - Creates AWS profiles programmatically
|
||||
- `scripts/migrate-aws-credentials.sh` - Migrates AWS credentials
|
||||
- `scripts/setup-aws-profiles.sh` - Sets up AWS profiles
|
||||
- `update-aws-creds.sh` - Updates AWS credentials
|
||||
|
||||
### 4. Runtime and Execution
|
||||
- `start-api.sh` - Starts the API server
|
||||
- `entrypoint.sh` - Container entrypoint script
|
||||
- `claudecode-entrypoint.sh` - Claude Code container entrypoint
|
||||
- `startup.sh` - Startup script
|
||||
- `claude-wrapper.sh` - Wrapper for Claude CLI
|
||||
|
||||
### 5. Network and Security
|
||||
- `init-firewall.sh` - Initializes firewall for containers
|
||||
- `accept-permissions.sh` - Handles permission acceptance
|
||||
- `fix-credential-references.sh` - Fixes credential references
|
||||
|
||||
### 6. Testing
|
||||
- `test/test-full-flow.sh` - Tests the full workflow
|
||||
- `test/test-claudecode-docker.sh` - Tests Claude Code Docker setup
|
||||
- `test/test-github-token.sh` - Tests GitHub token
|
||||
- `test/test-aws-profile.sh` - Tests AWS profile
|
||||
- `test/test-basic-container.sh` - Tests basic container functionality
|
||||
- `test/test-claude-direct.sh` - Tests direct Claude integration
|
||||
- `test/test-firewall.sh` - Tests firewall configuration
|
||||
- `test/test-direct-claude.sh` - Tests direct Claude access
|
||||
- `test/test-claude-no-firewall.sh` - Tests Claude without firewall
|
||||
- `test/test-claude-installation.sh` - Tests Claude installation
|
||||
- `test/test-aws-mount.sh` - Tests AWS mount functionality
|
||||
- `test/test-claude-version.sh` - Tests Claude version
|
||||
- `test/test-container-cleanup.sh` - Tests container cleanup
|
||||
- `test/test-claude-response.sh` - Tests Claude response
|
||||
- `test/test-container-privileged.sh` - Tests container privileged mode
|
||||
- `test/test-with-auth.sh` - Tests with authentication
|
||||
|
||||
### 7. Utility Scripts
|
||||
- `scripts/ensure-test-dirs.sh` - Ensures test directories exist
|
||||
- `prepare-clean-repo.sh` - Prepares a clean repository
|
||||
- `volume-test.sh` - Tests volume mounting
|
||||
|
||||
## Proposed Directory Structure
|
||||
|
||||
```
|
||||
/claude-repo
|
||||
├── scripts/
|
||||
│ ├── setup/
|
||||
│ │ ├── setup.sh
|
||||
│ │ ├── setup-precommit.sh
|
||||
│ │ ├── setup-claude-auth.sh
|
||||
│ │ ├── setup-new-repo.sh
|
||||
│ │ └── create-new-repo.sh
|
||||
│ ├── build/
|
||||
│ │ ├── build-claude-container.sh
|
||||
│ │ ├── build-claudecode.sh
|
||||
│ │ └── update-production-image.sh
|
||||
│ ├── aws/
|
||||
│ │ ├── create-aws-profile.sh
|
||||
│ │ ├── migrate-aws-credentials.sh
|
||||
│ │ ├── setup-aws-profiles.sh
|
||||
│ │ └── update-aws-creds.sh
|
||||
│ ├── runtime/
|
||||
│ │ ├── start-api.sh
|
||||
│ │ ├── entrypoint.sh
|
||||
│ │ ├── claudecode-entrypoint.sh
|
||||
│ │ ├── startup.sh
|
||||
│ │ └── claude-wrapper.sh
|
||||
│ ├── security/
|
||||
│ │ ├── init-firewall.sh
|
||||
│ │ ├── accept-permissions.sh
|
||||
│ │ └── fix-credential-references.sh
|
||||
│ └── utils/
|
||||
│ ├── ensure-test-dirs.sh
|
||||
│ ├── prepare-clean-repo.sh
|
||||
│ └── volume-test.sh
|
||||
├── test/
|
||||
│ ├── integration/
|
||||
│ │ ├── test-full-flow.sh
|
||||
│ │ ├── test-claudecode-docker.sh
|
||||
│ │ └── ...
|
||||
│ ├── aws/
|
||||
│ │ ├── test-aws-profile.sh
|
||||
│ │ ├── test-aws-mount.sh
|
||||
│ │ └── ...
|
||||
│ ├── container/
|
||||
│ │ ├── test-basic-container.sh
|
||||
│ │ ├── test-container-cleanup.sh
|
||||
│ │ ├── test-container-privileged.sh
|
||||
│ │ └── ...
|
||||
│ ├── claude/
|
||||
│ │ ├── test-claude-direct.sh
|
||||
│ │ ├── test-claude-no-firewall.sh
|
||||
│ │ ├── test-claude-installation.sh
|
||||
│ │ ├── test-claude-version.sh
|
||||
│ │ ├── test-claude-response.sh
|
||||
│ │ └── ...
|
||||
│ ├── security/
|
||||
│ │ ├── test-firewall.sh
|
||||
│ │ ├── test-with-auth.sh
|
||||
│ │ └── test-github-token.sh
|
||||
│ └── utils/
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Create the new directory structure
|
||||
2. Move scripts to their appropriate categories
|
||||
3. Update references in scripts to point to new locations
|
||||
4. Update documentation to reflect new organization
|
||||
5. Create wrapper scripts if needed to maintain backward compatibility
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing if Claude executable runs..."
|
||||
|
||||
docker run --rm \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "cd /workspace && /usr/local/share/npm-global/bin/claude --version 2>&1 || echo 'Exit code: $?'"
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing Claude directly without entrypoint..."
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
-v $HOME/.aws:/home/node/.aws:ro \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "cd /workspace && export HOME=/home/node && export PATH=/usr/local/share/npm-global/bin:\$PATH && export AWS_PROFILE=claude-webhook && export AWS_REGION=us-east-2 && export AWS_CONFIG_FILE=/home/node/.aws/config && export AWS_SHARED_CREDENTIALS_FILE=/home/node/.aws/credentials && export CLAUDE_CODE_USE_BEDROCK=1 && export ANTHROPIC_MODEL=us.anthropic.claude-3-7-sonnet-20250219-v1:0 && /usr/local/bin/init-firewall.sh && claude --print 'Hello world' 2>&1"
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Update AWS credentials in the environment
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-dummy-access-key}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-dummy-secret-key}"
|
||||
|
||||
# Create or update .env file with the new credentials
|
||||
if [ -f .env ]; then
|
||||
# Update existing .env file
|
||||
sed -i "s/^AWS_ACCESS_KEY_ID=.*/AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID/" .env
|
||||
sed -i "s/^AWS_SECRET_ACCESS_KEY=.*/AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY/" .env
|
||||
else
|
||||
# Create new .env file from example
|
||||
cp .env.example .env
|
||||
sed -i "s/^AWS_ACCESS_KEY_ID=.*/AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID/" .env
|
||||
sed -i "s/^AWS_SECRET_ACCESS_KEY=.*/AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY/" .env
|
||||
fi
|
||||
|
||||
echo "AWS credentials updated successfully."
|
||||
echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID"
|
||||
echo "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:0:3}...${AWS_SECRET_ACCESS_KEY:(-3)}"
|
||||
|
||||
# Export the credentials for current session
|
||||
export AWS_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
echo "Credentials exported to current shell environment."
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Migration script to transition from static AWS credentials to best practices
|
||||
|
||||
echo "AWS Credential Migration Script"
|
||||
echo "=============================="
|
||||
echo
|
||||
|
||||
# Function to check if running on EC2
|
||||
check_ec2() {
|
||||
if curl -s -m 1 http://169.254.169.254/latest/meta-data/ > /dev/null 2>&1; then
|
||||
echo "✅ Running on EC2 instance"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Not running on EC2 instance"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if running in ECS
|
||||
check_ecs() {
|
||||
if [ -n "${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}" ]; then
|
||||
echo "✅ Running in ECS with task role"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Not running in ECS"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check for static credentials
|
||||
check_static_credentials() {
|
||||
if [ -n "${AWS_ACCESS_KEY_ID}" ] && [ -n "${AWS_SECRET_ACCESS_KEY}" ]; then
|
||||
echo "⚠️ Found static AWS credentials in environment"
|
||||
return 0
|
||||
else
|
||||
echo "✅ No static credentials in environment"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to update .env file
|
||||
update_env_file() {
|
||||
if [ -f .env ]; then
|
||||
echo "Updating .env file..."
|
||||
|
||||
# Comment out static credentials
|
||||
sed -i 's/^AWS_ACCESS_KEY_ID=/#AWS_ACCESS_KEY_ID=/' .env
|
||||
sed -i 's/^AWS_SECRET_ACCESS_KEY=/#AWS_SECRET_ACCESS_KEY=/' .env
|
||||
|
||||
# Add migration notes
|
||||
echo "" >> .env
|
||||
echo "# AWS Credentials migrated to use IAM roles/instance profiles" >> .env
|
||||
echo "# See docs/aws-authentication-best-practices.md for details" >> .env
|
||||
echo "" >> .env
|
||||
|
||||
echo "✅ Updated .env file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main migration process
|
||||
echo "1. Checking current environment..."
|
||||
echo
|
||||
|
||||
if check_ec2; then
|
||||
echo " Recommendation: Use IAM instance profile"
|
||||
echo " The application will automatically use instance metadata"
|
||||
elif check_ecs; then
|
||||
echo " Recommendation: Use ECS task role"
|
||||
echo " The application will automatically use task credentials"
|
||||
else
|
||||
echo " Recommendation: Use temporary credentials with STS AssumeRole"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "2. Checking for static credentials..."
|
||||
echo
|
||||
|
||||
if check_static_credentials; then
|
||||
echo " ⚠️ WARNING: Static credentials should be replaced with temporary credentials"
|
||||
echo
|
||||
read -p " Do you want to disable static credentials? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
update_env_file
|
||||
echo
|
||||
echo " To use temporary credentials, configure:"
|
||||
echo " - AWS_ROLE_ARN: The IAM role to assume"
|
||||
echo " - Or use AWS CLI profiles with assume role"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "3. Testing new credential provider..."
|
||||
echo
|
||||
|
||||
# Test the credential provider
|
||||
node test/test-aws-credential-provider.js
|
||||
|
||||
echo
|
||||
echo "Migration complete!"
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo "1. Review docs/aws-authentication-best-practices.md"
|
||||
echo "2. Update your deployment configuration"
|
||||
echo "3. Test the application with new credential provider"
|
||||
echo "4. Remove update-aws-creds.sh script (no longer needed)"
|
||||
echo
|
||||
|
||||
# Check if update-aws-creds.sh exists and suggest removal
|
||||
if [ -f update-aws-creds.sh ]; then
|
||||
echo "⚠️ Found update-aws-creds.sh - this script is no longer needed"
|
||||
read -p "Do you want to remove it? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
rm update-aws-creds.sh
|
||||
echo "✅ Removed update-aws-creds.sh"
|
||||
fi
|
||||
fi
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build the Claude Code container
|
||||
echo "Building Claude Code container..."
|
||||
docker build -t claudecode:latest -f Dockerfile.claude .
|
||||
|
||||
echo "Container built successfully. You can run it with:"
|
||||
echo "docker run --rm claudecode:latest \"claude --help\""
|
||||
|
||||
# Enable container mode in the .env file if it's not already set
|
||||
if ! grep -q "CLAUDE_USE_CONTAINERS=1" .env 2>/dev/null; then
|
||||
echo ""
|
||||
echo "Enabling container mode in .env file..."
|
||||
echo "CLAUDE_USE_CONTAINERS=1" >> .env
|
||||
echo "CLAUDE_CONTAINER_IMAGE=claudecode:latest" >> .env
|
||||
echo "Container mode enabled in .env file"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done! You can now use the Claude API with container mode."
|
||||
echo "To test it, run:"
|
||||
echo "node test-claude-api.js owner/repo container \"Your command here\""
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build the Claude Code runner Docker image
|
||||
|
||||
echo "Building Claude Code runner Docker image..."
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:latest .
|
||||
|
||||
echo "Build complete!"
|
||||
@@ -14,7 +14,7 @@ case "$BUILD_TYPE" in
|
||||
|
||||
claudecode)
|
||||
echo "Building Claude Code runner Docker image..."
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:latest .
|
||||
docker build -f Dockerfile.claudecode -t claudecode:latest .
|
||||
;;
|
||||
|
||||
production)
|
||||
@@ -25,10 +25,106 @@ case "$BUILD_TYPE" in
|
||||
fi
|
||||
|
||||
echo "Building production image with pre-authenticated config..."
|
||||
cp Dockerfile.claudecode Dockerfile.claudecode.backup
|
||||
# Production build logic from update-production-image.sh
|
||||
# ... (truncated for brevity)
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:production .
|
||||
|
||||
# Create a temporary production Dockerfile with claude-config enabled
|
||||
cat > Dockerfile.claudecode.prod << 'EOF'
|
||||
FROM node:24
|
||||
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y less \
|
||||
git \
|
||||
procps \
|
||||
sudo \
|
||||
fzf \
|
||||
zsh \
|
||||
man-db \
|
||||
unzip \
|
||||
gnupg2 \
|
||||
gh \
|
||||
iptables \
|
||||
ipset \
|
||||
iproute2 \
|
||||
dnsutils \
|
||||
aggregate \
|
||||
jq
|
||||
|
||||
# Set up npm global directory
|
||||
RUN mkdir -p /usr/local/share/npm-global && \
|
||||
chown -R node:node /usr/local/share
|
||||
|
||||
# Configure zsh and command history
|
||||
ENV USERNAME=node
|
||||
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
||||
&& mkdir /commandhistory \
|
||||
&& touch /commandhistory/.bash_history \
|
||||
&& chown -R $USERNAME /commandhistory
|
||||
|
||||
# Create workspace and config directories
|
||||
RUN mkdir -p /workspace /home/node/.claude && \
|
||||
chown -R node:node /workspace /home/node/.claude
|
||||
|
||||
# Switch to node user temporarily for npm install
|
||||
USER node
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
|
||||
# Install Claude Code
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Switch back to root
|
||||
USER root
|
||||
|
||||
# Copy the pre-authenticated Claude config to BOTH root and node user (PRODUCTION ONLY)
|
||||
COPY claude-config /root/.claude
|
||||
COPY claude-config /home/node/.claude
|
||||
RUN chown -R node:node /home/node/.claude
|
||||
|
||||
# Copy the rest of the setup
|
||||
WORKDIR /workspace
|
||||
|
||||
# Install delta and zsh
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \
|
||||
sudo dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \
|
||||
rm "git-delta_0.18.2_${ARCH}.deb"
|
||||
|
||||
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \
|
||||
-p git \
|
||||
-p fzf \
|
||||
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
|
||||
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
|
||||
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
||||
-x
|
||||
|
||||
# Copy firewall and entrypoint scripts
|
||||
COPY scripts/security/init-firewall.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/init-firewall.sh && \
|
||||
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
|
||||
chmod 0440 /etc/sudoers.d/node-firewall
|
||||
|
||||
# Create scripts directory and copy unified entrypoint script
|
||||
RUN mkdir -p /scripts/runtime
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /scripts/runtime/claudecode-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh && \
|
||||
chmod +x /scripts/runtime/claudecode-entrypoint.sh
|
||||
|
||||
# Set the default shell to bash
|
||||
ENV SHELL /bin/zsh
|
||||
ENV DEVCONTAINER=true
|
||||
|
||||
# Run as root to allow permission management
|
||||
USER root
|
||||
|
||||
# Use the custom entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
EOF
|
||||
|
||||
# Build the production image
|
||||
docker build -f Dockerfile.claudecode.prod -t claudecode:production .
|
||||
|
||||
# Clean up temporary file
|
||||
rm -f Dockerfile.claudecode.prod
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ ! -d "./claude-config" ]; then
|
||||
echo "Error: claude-config directory not found."
|
||||
echo "Please run ./setup-claude-auth.sh first and copy the config."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updating Dockerfile.claudecode to include pre-authenticated config..."
|
||||
|
||||
# Create a backup of the original Dockerfile
|
||||
cp Dockerfile.claudecode Dockerfile.claudecode.backup
|
||||
|
||||
# Update the Dockerfile to copy the claude config
|
||||
cat > Dockerfile.claudecode.tmp << 'EOF'
|
||||
FROM node:20
|
||||
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y less \
|
||||
git \
|
||||
procps \
|
||||
sudo \
|
||||
fzf \
|
||||
zsh \
|
||||
man-db \
|
||||
unzip \
|
||||
gnupg2 \
|
||||
gh \
|
||||
iptables \
|
||||
ipset \
|
||||
iproute2 \
|
||||
dnsutils \
|
||||
aggregate \
|
||||
jq
|
||||
|
||||
# Set up npm global directory
|
||||
RUN mkdir -p /usr/local/share/npm-global && \
|
||||
chown -R node:node /usr/local/share
|
||||
|
||||
# Configure zsh and command history
|
||||
ENV USERNAME=node
|
||||
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
||||
&& mkdir /commandhistory \
|
||||
&& touch /commandhistory/.bash_history \
|
||||
&& chown -R $USERNAME /commandhistory
|
||||
|
||||
# Create workspace and config directories
|
||||
RUN mkdir -p /workspace /home/node/.claude && \
|
||||
chown -R node:node /workspace /home/node/.claude
|
||||
|
||||
# Switch to node user temporarily for npm install
|
||||
USER node
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
|
||||
# Install Claude Code
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Switch back to root
|
||||
USER root
|
||||
|
||||
# Copy the pre-authenticated Claude config
|
||||
COPY claude-config /root/.claude
|
||||
|
||||
# Copy the rest of the setup
|
||||
WORKDIR /workspace
|
||||
|
||||
# Install delta and zsh
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \
|
||||
sudo dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \
|
||||
rm "git-delta_0.18.2_${ARCH}.deb"
|
||||
|
||||
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \
|
||||
-p git \
|
||||
-p fzf \
|
||||
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
|
||||
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
|
||||
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
||||
-x
|
||||
|
||||
# Copy firewall and entrypoint scripts
|
||||
COPY init-firewall.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/init-firewall.sh && \
|
||||
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
|
||||
chmod 0440 /etc/sudoers.d/node-firewall
|
||||
|
||||
COPY claudecode-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Set the default shell to bash
|
||||
ENV SHELL /bin/zsh
|
||||
ENV DEVCONTAINER=true
|
||||
|
||||
# Run as root to allow permission management
|
||||
USER root
|
||||
|
||||
# Use the custom entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
EOF
|
||||
|
||||
mv Dockerfile.claudecode.tmp Dockerfile.claudecode
|
||||
|
||||
echo "Building new production image..."
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:latest .
|
||||
|
||||
echo "Production image updated successfully!"
|
||||
@@ -1,336 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# GitHub Actions Runner Management Script
|
||||
# Manage the webhook deployment runner service
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
SERVICE_NAME="webhook-deployment-runner"
|
||||
RUNNER_DIR="/home/jonflatt/github-actions-runner"
|
||||
RUNNER_USER="jonflatt"
|
||||
|
||||
# Function to print usage
|
||||
usage() {
|
||||
echo -e "${BLUE}GitHub Actions Runner Management Tool${NC}"
|
||||
echo -e "${BLUE}=====================================${NC}"
|
||||
echo -e "\nUsage: $0 [command]"
|
||||
echo -e "\nCommands:"
|
||||
echo -e " ${GREEN}start${NC} - Start the runner service"
|
||||
echo -e " ${GREEN}stop${NC} - Stop the runner service"
|
||||
echo -e " ${GREEN}restart${NC} - Restart the runner service"
|
||||
echo -e " ${GREEN}status${NC} - Check runner service status"
|
||||
echo -e " ${GREEN}logs${NC} - View runner logs (live)"
|
||||
echo -e " ${GREEN}logs-tail${NC} - View last 50 lines of logs"
|
||||
echo -e " ${GREEN}update${NC} - Update runner to latest version"
|
||||
echo -e " ${GREEN}config${NC} - Show runner configuration"
|
||||
echo -e " ${GREEN}health${NC} - Check runner health"
|
||||
echo -e " ${GREEN}jobs${NC} - Show recent job history"
|
||||
echo -e " ${GREEN}cleanup${NC} - Clean up work directory"
|
||||
echo -e " ${GREEN}info${NC} - Show runner information"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if running with correct permissions
|
||||
check_permissions() {
|
||||
if [[ $EUID -ne 0 ]] && [[ "$1" =~ ^(start|stop|restart|update)$ ]]; then
|
||||
echo -e "${RED}Error: This command requires sudo privileges${NC}"
|
||||
echo -e "${YELLOW}Run: sudo $0 $1${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the runner
|
||||
start_runner() {
|
||||
echo -e "${YELLOW}Starting runner service...${NC}"
|
||||
systemctl start $SERVICE_NAME
|
||||
sleep 2
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}✓ Runner started successfully${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager | head -n 10
|
||||
else
|
||||
echo -e "${RED}✗ Failed to start runner${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop the runner
|
||||
stop_runner() {
|
||||
echo -e "${YELLOW}Stopping runner service...${NC}"
|
||||
systemctl stop $SERVICE_NAME
|
||||
echo -e "${GREEN}✓ Runner stopped${NC}"
|
||||
}
|
||||
|
||||
# Restart the runner
|
||||
restart_runner() {
|
||||
echo -e "${YELLOW}Restarting runner service...${NC}"
|
||||
systemctl restart $SERVICE_NAME
|
||||
sleep 2
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}✓ Runner restarted successfully${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager | head -n 10
|
||||
else
|
||||
echo -e "${RED}✗ Failed to restart runner${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check runner status
|
||||
check_status() {
|
||||
echo -e "${BLUE}Runner Service Status${NC}"
|
||||
echo -e "${BLUE}===================${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager
|
||||
|
||||
echo -e "\n${BLUE}Runner Process Info${NC}"
|
||||
echo -e "${BLUE}===================${NC}"
|
||||
ps aux | grep -E "(Runner.Listener|run.sh)" | grep -v grep || echo "No runner processes found"
|
||||
}
|
||||
|
||||
# View logs
|
||||
view_logs() {
|
||||
echo -e "${YELLOW}Viewing live logs (Ctrl+C to exit)...${NC}"
|
||||
journalctl -u $SERVICE_NAME -f
|
||||
}
|
||||
|
||||
# View last 50 lines of logs
|
||||
view_logs_tail() {
|
||||
echo -e "${BLUE}Last 50 lines of runner logs${NC}"
|
||||
echo -e "${BLUE}===========================${NC}"
|
||||
journalctl -u $SERVICE_NAME -n 50 --no-pager
|
||||
}
|
||||
|
||||
# Update runner
|
||||
update_runner() {
|
||||
echo -e "${YELLOW}Updating GitHub Actions Runner...${NC}"
|
||||
|
||||
# Stop the service
|
||||
systemctl stop $SERVICE_NAME
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$($RUNNER_DIR/bin/Runner.Listener --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown")
|
||||
echo -e "Current version: ${YELLOW}$CURRENT_VERSION${NC}"
|
||||
|
||||
# Get latest version
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
|
||||
echo -e "Latest version: ${GREEN}$LATEST_VERSION${NC}"
|
||||
|
||||
if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then
|
||||
echo -e "${GREEN}✓ Runner is already up to date${NC}"
|
||||
systemctl start $SERVICE_NAME
|
||||
return
|
||||
fi
|
||||
|
||||
# Backup current runner
|
||||
echo -e "${YELLOW}Backing up current runner...${NC}"
|
||||
cd $RUNNER_DIR
|
||||
tar -czf runner-backup-$(date +%Y%m%d-%H%M%S).tar.gz bin externals
|
||||
|
||||
# Download and extract new version
|
||||
echo -e "${YELLOW}Downloading new version...${NC}"
|
||||
curl -o actions-runner-linux-x64.tar.gz -L "https://github.com/actions/runner/releases/download/v${LATEST_VERSION}/actions-runner-linux-x64-${LATEST_VERSION}.tar.gz"
|
||||
tar xzf ./actions-runner-linux-x64.tar.gz
|
||||
rm actions-runner-linux-x64.tar.gz
|
||||
|
||||
# Start the service
|
||||
systemctl start $SERVICE_NAME
|
||||
echo -e "${GREEN}✓ Runner updated to version $LATEST_VERSION${NC}"
|
||||
}
|
||||
|
||||
# Show configuration
|
||||
show_config() {
|
||||
echo -e "${BLUE}Runner Configuration${NC}"
|
||||
echo -e "${BLUE}===================${NC}"
|
||||
|
||||
if [ -f "$RUNNER_DIR/.runner" ]; then
|
||||
echo -e "\n${GREEN}Runner Settings:${NC}"
|
||||
cat "$RUNNER_DIR/.runner" | jq '.' 2>/dev/null || cat "$RUNNER_DIR/.runner"
|
||||
fi
|
||||
|
||||
if [ -f "$RUNNER_DIR/.credentials" ]; then
|
||||
echo -e "\n${GREEN}Runner Registration:${NC}"
|
||||
echo "Runner is registered (credentials file exists)"
|
||||
else
|
||||
echo -e "\n${RED}Runner is not configured${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}Service Configuration:${NC}"
|
||||
systemctl show $SERVICE_NAME | grep -E "(LoadState|ActiveState|SubState|MainPID|Environment)"
|
||||
}
|
||||
|
||||
# Check health
|
||||
check_health() {
|
||||
echo -e "${BLUE}Runner Health Check${NC}"
|
||||
echo -e "${BLUE}==================${NC}"
|
||||
|
||||
# Check service status
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}✓ Service is running${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Service is not running${NC}"
|
||||
fi
|
||||
|
||||
# Check disk space
|
||||
DISK_USAGE=$(df -h $RUNNER_DIR | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||
if [ "$DISK_USAGE" -lt 80 ]; then
|
||||
echo -e "${GREEN}✓ Disk usage: ${DISK_USAGE}%${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Disk usage: ${DISK_USAGE}% (High)${NC}"
|
||||
fi
|
||||
|
||||
# Check work directory size
|
||||
if [ -d "$RUNNER_DIR/_work" ]; then
|
||||
WORK_SIZE=$(du -sh "$RUNNER_DIR/_work" 2>/dev/null | cut -f1)
|
||||
echo -e "${BLUE}Work directory size: $WORK_SIZE${NC}"
|
||||
fi
|
||||
|
||||
# Check runner connectivity
|
||||
if [ -f "$RUNNER_DIR/.runner" ]; then
|
||||
GITHUB_URL=$(cat "$RUNNER_DIR/.runner" | jq -r '.gitHubUrl' 2>/dev/null || echo "")
|
||||
if [ -n "$GITHUB_URL" ] && curl -s -o /dev/null -w "%{http_code}" "$GITHUB_URL" | grep -q "200"; then
|
||||
echo -e "${GREEN}✓ GitHub connectivity OK${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Cannot verify GitHub connectivity${NC}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Show recent jobs
|
||||
show_jobs() {
|
||||
echo -e "${BLUE}Recent Runner Jobs${NC}"
|
||||
echo -e "${BLUE}=================${NC}"
|
||||
|
||||
# Check for job history in work directory
|
||||
if [ -d "$RUNNER_DIR/_work" ]; then
|
||||
echo -e "\n${GREEN}Recent job directories:${NC}"
|
||||
ls -la "$RUNNER_DIR/_work" 2>/dev/null | tail -n 10 || echo "No job directories found"
|
||||
fi
|
||||
|
||||
# Show recent log entries
|
||||
echo -e "\n${GREEN}Recent job activity:${NC}"
|
||||
journalctl -u $SERVICE_NAME --since "1 hour ago" | grep -E "(Running job|Job .* completed|Completed request)" | tail -n 20 || echo "No recent job activity"
|
||||
}
|
||||
|
||||
# Cleanup work directory
|
||||
cleanup_work() {
|
||||
echo -e "${YELLOW}Cleaning up work directory...${NC}"
|
||||
|
||||
if [ ! -d "$RUNNER_DIR/_work" ]; then
|
||||
echo -e "${GREEN}Work directory doesn't exist${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Show current size
|
||||
BEFORE_SIZE=$(du -sh "$RUNNER_DIR/_work" 2>/dev/null | cut -f1)
|
||||
echo -e "Current size: ${YELLOW}$BEFORE_SIZE${NC}"
|
||||
|
||||
# Confirm
|
||||
read -p "Are you sure you want to clean the work directory? (y/N): " confirm
|
||||
if [ "$confirm" != "y" ]; then
|
||||
echo -e "${YELLOW}Cleanup cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Stop runner
|
||||
systemctl stop $SERVICE_NAME
|
||||
|
||||
# Clean work directory
|
||||
rm -rf "$RUNNER_DIR/_work"/*
|
||||
|
||||
# Start runner
|
||||
systemctl start $SERVICE_NAME
|
||||
|
||||
echo -e "${GREEN}✓ Work directory cleaned${NC}"
|
||||
}
|
||||
|
||||
# Show runner info
|
||||
show_info() {
|
||||
echo -e "${BLUE}GitHub Actions Runner Information${NC}"
|
||||
echo -e "${BLUE}=================================${NC}"
|
||||
|
||||
echo -e "\n${GREEN}Basic Info:${NC}"
|
||||
echo -e "Service Name: ${YELLOW}$SERVICE_NAME${NC}"
|
||||
echo -e "Runner Directory: ${YELLOW}$RUNNER_DIR${NC}"
|
||||
echo -e "Runner User: ${YELLOW}$RUNNER_USER${NC}"
|
||||
|
||||
if [ -f "$RUNNER_DIR/bin/Runner.Listener" ]; then
|
||||
VERSION=$($RUNNER_DIR/bin/Runner.Listener --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown")
|
||||
echo -e "Runner Version: ${YELLOW}$VERSION${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}System Info:${NC}"
|
||||
echo -e "Hostname: ${YELLOW}$(hostname)${NC}"
|
||||
echo -e "OS: ${YELLOW}$(lsb_release -d | cut -f2)${NC}"
|
||||
echo -e "Kernel: ${YELLOW}$(uname -r)${NC}"
|
||||
echo -e "Architecture: ${YELLOW}$(uname -m)${NC}"
|
||||
|
||||
echo -e "\n${GREEN}Docker Info:${NC}"
|
||||
if command -v docker &> /dev/null; then
|
||||
DOCKER_VERSION=$(docker --version | awk '{print $3}' | sed 's/,$//')
|
||||
echo -e "Docker Version: ${YELLOW}$DOCKER_VERSION${NC}"
|
||||
|
||||
if groups $RUNNER_USER | grep -q docker; then
|
||||
echo -e "Docker Access: ${GREEN}✓ User in docker group${NC}"
|
||||
else
|
||||
echo -e "Docker Access: ${RED}✗ User not in docker group${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Docker not installed${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}Labels:${NC}"
|
||||
echo -e "${YELLOW}self-hosted,linux,x64,deployment,webhook-cd${NC}"
|
||||
}
|
||||
|
||||
# Main logic
|
||||
check_permissions "$1"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_runner
|
||||
;;
|
||||
stop)
|
||||
stop_runner
|
||||
;;
|
||||
restart)
|
||||
restart_runner
|
||||
;;
|
||||
status)
|
||||
check_status
|
||||
;;
|
||||
logs)
|
||||
view_logs
|
||||
;;
|
||||
logs-tail)
|
||||
view_logs_tail
|
||||
;;
|
||||
update)
|
||||
update_runner
|
||||
;;
|
||||
config)
|
||||
show_config
|
||||
;;
|
||||
health)
|
||||
check_health
|
||||
;;
|
||||
jobs)
|
||||
show_jobs
|
||||
;;
|
||||
cleanup)
|
||||
cleanup_work
|
||||
;;
|
||||
info)
|
||||
show_info
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Unified entrypoint for Claude Code operations
|
||||
# Handles both auto-tagging (minimal tools) and general operations (full tools)
|
||||
# Operation type is controlled by OPERATION_TYPE environment variable
|
||||
|
||||
# Initialize firewall - must be done as root
|
||||
# Temporarily disabled to test Claude Code
|
||||
# /usr/local/bin/init-firewall.sh
|
||||
@@ -13,6 +17,42 @@ set -e
|
||||
mkdir -p /workspace
|
||||
chown -R node:node /workspace
|
||||
|
||||
# Set up Claude authentication by syncing from captured auth directory
|
||||
if [ -d "/home/node/.claude" ]; then
|
||||
echo "Setting up Claude authentication from mounted auth directory..." >&2
|
||||
|
||||
# Create a writable copy of Claude configuration in workspace
|
||||
CLAUDE_WORK_DIR="/workspace/.claude"
|
||||
mkdir -p "$CLAUDE_WORK_DIR"
|
||||
|
||||
echo "DEBUG: Source auth directory contents:" >&2
|
||||
ls -la /home/node/.claude/ >&2 || echo "DEBUG: Source auth directory not accessible" >&2
|
||||
|
||||
# Sync entire auth directory to writable location (including database files, project state, etc.)
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -av /home/node/.claude/ "$CLAUDE_WORK_DIR/" 2>/dev/null || echo "rsync failed, trying cp" >&2
|
||||
else
|
||||
# Fallback to cp with comprehensive copying
|
||||
cp -r /home/node/.claude/* "$CLAUDE_WORK_DIR/" 2>/dev/null || true
|
||||
cp -r /home/node/.claude/.* "$CLAUDE_WORK_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "DEBUG: Working directory contents after sync:" >&2
|
||||
ls -la "$CLAUDE_WORK_DIR/" >&2 || echo "DEBUG: Working directory not accessible" >&2
|
||||
|
||||
# Set proper ownership and permissions for the node user
|
||||
chown -R node:node "$CLAUDE_WORK_DIR"
|
||||
chmod 600 "$CLAUDE_WORK_DIR"/.credentials.json 2>/dev/null || true
|
||||
chmod 755 "$CLAUDE_WORK_DIR" 2>/dev/null || true
|
||||
|
||||
echo "DEBUG: Final permissions check:" >&2
|
||||
ls -la "$CLAUDE_WORK_DIR/.credentials.json" >&2 || echo "DEBUG: .credentials.json not found" >&2
|
||||
|
||||
echo "Claude authentication directory synced to $CLAUDE_WORK_DIR" >&2
|
||||
else
|
||||
echo "WARNING: No Claude authentication source found at /home/node/.claude." >&2
|
||||
fi
|
||||
|
||||
# Configure GitHub authentication
|
||||
if [ -n "${GITHUB_TOKEN}" ]; then
|
||||
export GH_TOKEN="${GITHUB_TOKEN}"
|
||||
@@ -32,8 +72,12 @@ else
|
||||
cd /workspace
|
||||
fi
|
||||
|
||||
# Checkout the correct branch
|
||||
if [ "${IS_PULL_REQUEST}" = "true" ] && [ -n "${BRANCH_NAME}" ]; then
|
||||
# Checkout the correct branch based on operation type
|
||||
if [ "${OPERATION_TYPE}" = "auto-tagging" ]; then
|
||||
# Auto-tagging always uses main branch (doesn't need specific branches)
|
||||
echo "Using main branch for auto-tagging" >&2
|
||||
sudo -u node git checkout main >&2 || sudo -u node git checkout master >&2
|
||||
elif [ "${IS_PULL_REQUEST}" = "true" ] && [ -n "${BRANCH_NAME}" ]; then
|
||||
echo "Checking out PR branch: ${BRANCH_NAME}" >&2
|
||||
sudo -u node git checkout "${BRANCH_NAME}" >&2
|
||||
else
|
||||
@@ -45,16 +89,46 @@ fi
|
||||
sudo -u node git config --global user.email "${BOT_EMAIL:-claude@example.com}"
|
||||
sudo -u node git config --global user.name "${BOT_USERNAME:-ClaudeBot}"
|
||||
|
||||
# Configure Anthropic API key
|
||||
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
|
||||
# Configure Claude authentication
|
||||
# Support both API key and interactive auth methods
|
||||
echo "DEBUG: Checking authentication options..." >&2
|
||||
echo "DEBUG: ANTHROPIC_API_KEY set: $([ -n "${ANTHROPIC_API_KEY}" ] && echo 'YES' || echo 'NO')" >&2
|
||||
echo "DEBUG: /workspace/.claude/.credentials.json exists: $([ -f "/workspace/.claude/.credentials.json" ] && echo 'YES' || echo 'NO')" >&2
|
||||
echo "DEBUG: /workspace/.claude contents:" >&2
|
||||
ls -la /workspace/.claude/ >&2 || echo "DEBUG: /workspace/.claude directory not found" >&2
|
||||
|
||||
if [ -n "${ANTHROPIC_API_KEY}" ]; then
|
||||
echo "Using Anthropic API key for authentication..." >&2
|
||||
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
|
||||
elif [ -f "/workspace/.claude/.credentials.json" ]; then
|
||||
echo "Using Claude interactive authentication from working directory..." >&2
|
||||
# No need to set ANTHROPIC_API_KEY - Claude CLI will use the credentials file
|
||||
# Set HOME to point to our working directory for Claude CLI
|
||||
export CLAUDE_HOME="/workspace/.claude"
|
||||
echo "DEBUG: Set CLAUDE_HOME to $CLAUDE_HOME" >&2
|
||||
else
|
||||
echo "WARNING: No Claude authentication found. Please set ANTHROPIC_API_KEY or ensure ~/.claude is mounted." >&2
|
||||
fi
|
||||
|
||||
# Create response file with proper permissions
|
||||
RESPONSE_FILE="/workspace/response.txt"
|
||||
touch "${RESPONSE_FILE}"
|
||||
chown node:node "${RESPONSE_FILE}"
|
||||
|
||||
# Run Claude Code with full GitHub CLI access as node user
|
||||
echo "Running Claude Code..." >&2
|
||||
# Determine allowed tools based on operation type
|
||||
if [ "${OPERATION_TYPE}" = "auto-tagging" ]; then
|
||||
ALLOWED_TOOLS="Read,GitHub,Bash(gh issue edit:*),Bash(gh issue view:*),Bash(gh label list:*)" # Minimal tools for auto-tagging (security)
|
||||
echo "Running Claude Code for auto-tagging with minimal tools..." >&2
|
||||
elif [ "${OPERATION_TYPE}" = "pr-review" ] || [ "${OPERATION_TYPE}" = "manual-pr-review" ]; then
|
||||
# PR Review: Broad research access + controlled write access
|
||||
# Read access: Full file system, git history, GitHub data
|
||||
# Write access: GitHub comments/reviews, PR labels, but no file deletion/modification
|
||||
ALLOWED_TOOLS="Read,GitHub,Bash(gh:*),Bash(git log:*),Bash(git show:*),Bash(git diff:*),Bash(git blame:*),Bash(find:*),Bash(grep:*),Bash(rg:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(ls:*),Bash(tree:*)"
|
||||
echo "Running Claude Code for PR review with broad research access..." >&2
|
||||
else
|
||||
ALLOWED_TOOLS="Bash,Create,Edit,Read,Write,GitHub" # Full tools for general operations
|
||||
echo "Running Claude Code with full tool access..." >&2
|
||||
fi
|
||||
|
||||
# Check if command exists
|
||||
if [ -z "${COMMAND}" ]; then
|
||||
@@ -65,14 +139,27 @@ fi
|
||||
# Log the command length for debugging
|
||||
echo "Command length: ${#COMMAND}" >&2
|
||||
|
||||
# Run Claude Code
|
||||
# Run Claude Code with proper HOME environment
|
||||
# If we synced Claude auth to workspace, use workspace as HOME
|
||||
if [ -f "/workspace/.claude/.credentials.json" ]; then
|
||||
CLAUDE_USER_HOME="/workspace"
|
||||
echo "DEBUG: Using /workspace as HOME for Claude CLI (synced auth)" >&2
|
||||
else
|
||||
CLAUDE_USER_HOME="${CLAUDE_HOME:-/home/node}"
|
||||
echo "DEBUG: Using $CLAUDE_USER_HOME as HOME for Claude CLI (fallback)" >&2
|
||||
fi
|
||||
|
||||
sudo -u node -E env \
|
||||
HOME="/home/node" \
|
||||
HOME="$CLAUDE_USER_HOME" \
|
||||
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
|
||||
GH_TOKEN="${GITHUB_TOKEN}" \
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
||||
BASH_DEFAULT_TIMEOUT_MS="${BASH_DEFAULT_TIMEOUT_MS}" \
|
||||
BASH_MAX_TIMEOUT_MS="${BASH_MAX_TIMEOUT_MS}" \
|
||||
/usr/local/share/npm-global/bin/claude \
|
||||
--allowedTools Bash,Create,Edit,Read,Write,GitHub \
|
||||
--allowedTools "${ALLOWED_TOOLS}" \
|
||||
--verbose \
|
||||
--print "${COMMAND}" \
|
||||
> "${RESPONSE_FILE}" 2>&1
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get port from environment or default to 3003
|
||||
DEFAULT_PORT=${PORT:-3003}
|
||||
# Load environment variables from .env file if it exists
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Get port from environment or default to 3002
|
||||
DEFAULT_PORT=${PORT:-3002}
|
||||
|
||||
# Kill any processes using the port
|
||||
echo "Checking for existing processes on port $DEFAULT_PORT..."
|
||||
@@ -13,4 +20,4 @@ fi
|
||||
|
||||
# Start the server with the specified port
|
||||
echo "Starting server on port $DEFAULT_PORT..."
|
||||
PORT=$DEFAULT_PORT node src/index.js
|
||||
PORT=$DEFAULT_PORT node dist/index.js
|
||||
@@ -2,14 +2,24 @@
|
||||
|
||||
echo "Starting Claude GitHub webhook service..."
|
||||
|
||||
# Build the Claude Code runner image
|
||||
echo "Building Claude Code runner image..."
|
||||
if docker build -f Dockerfile.claudecode -t claude-code-runner:latest .; then
|
||||
echo "Claude Code runner image built successfully."
|
||||
# Build the Claude Code runner image if we have access to Dockerfile.claudecode
|
||||
if [ -f "Dockerfile.claudecode" ]; then
|
||||
echo "Building Claude Code runner image..."
|
||||
if docker build -f Dockerfile.claudecode -t claude-code-runner:latest .; then
|
||||
echo "Claude Code runner image built successfully."
|
||||
else
|
||||
echo "Warning: Failed to build Claude Code runner image. Service will attempt to build on first use."
|
||||
fi
|
||||
else
|
||||
echo "Warning: Failed to build Claude Code runner image. Service will attempt to build on first use."
|
||||
echo "Dockerfile.claudecode not found, skipping Claude Code runner image build."
|
||||
fi
|
||||
|
||||
# In production, dist directory is already built in the Docker image
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "Error: dist directory not found. Please rebuild the Docker image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start the webhook service
|
||||
echo "Starting webhook service..."
|
||||
exec node src/index.js
|
||||
exec node dist/index.js
|
||||
@@ -32,8 +32,8 @@ report_success() {
|
||||
|
||||
# 1. Check for .env files that shouldn't be committed
|
||||
echo "🔍 Checking for exposed .env files..."
|
||||
if find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" | grep -q .; then
|
||||
find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" | while read file; do
|
||||
if find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" -not -name ".env.quickstart" | grep -q .; then
|
||||
find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" -not -name ".env.quickstart" | while read file; do
|
||||
report_issue "Found .env file that may contain secrets: $file"
|
||||
done
|
||||
else
|
||||
@@ -51,7 +51,7 @@ CREDENTIAL_PATTERNS=(
|
||||
)
|
||||
|
||||
for pattern in "${CREDENTIAL_PATTERNS[@]}"; do
|
||||
if grep -rE "$pattern" --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=coverage --exclude="credential-audit.sh" . 2>/dev/null; then
|
||||
if grep -rE "$pattern" --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=coverage --exclude="credential-audit.sh" --exclude="test-logger-redaction.js" --exclude="test-logger-redaction-comprehensive.js" . 2>/dev/null; then
|
||||
report_issue "Found potential hardcoded credentials matching pattern: $pattern"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script to fix potential credential references in the clean repository
|
||||
|
||||
CLEAN_REPO="/tmp/clean-repo"
|
||||
cd "$CLEAN_REPO" || exit 1
|
||||
|
||||
echo "Fixing potential credential references..."
|
||||
|
||||
# 1. Fix test files with example tokens
|
||||
echo "Updating test-credential-leak.js..."
|
||||
sed -i 's/ghp_verySecretGitHubToken123456789/github_token_example_1234567890/g' test-credential-leak.js
|
||||
|
||||
echo "Updating test-logger-redaction.js..."
|
||||
sed -i 's/ghp_verySecretGitHubToken123456789/github_token_example_1234567890/g' test/test-logger-redaction.js
|
||||
sed -i 's/ghp_nestedSecretToken/github_token_example_nested/g' test/test-logger-redaction.js
|
||||
sed -i 's/ghp_inCommand/github_token_example_command/g' test/test-logger-redaction.js
|
||||
sed -i 's/ghp_errorToken/github_token_example_error/g' test/test-logger-redaction.js
|
||||
sed -i 's/AKIAIOSFODNN7NESTED/EXAMPLE_NESTED_KEY_ID/g' test/test-logger-redaction.js
|
||||
|
||||
echo "Updating test-secrets.js..."
|
||||
sed -i 's/ghp_1234567890abcdefghijklmnopqrstuvwxy/github_token_example_1234567890/g' test/test-secrets.js
|
||||
|
||||
# 2. Fix references in documentation
|
||||
echo "Updating docs/container-setup.md..."
|
||||
sed -i 's/GITHUB_TOKEN=ghp_yourgithubtoken/GITHUB_TOKEN=your_github_token/g' docs/container-setup.md
|
||||
|
||||
echo "Updating docs/complete-workflow.md..."
|
||||
sed -i 's/`ghp_xxxxx`/`your_github_token`/g' docs/complete-workflow.md
|
||||
sed -i 's/`AKIA...`/`your_access_key_id`/g' docs/complete-workflow.md
|
||||
|
||||
# 3. Update AWS profile references in scripts
|
||||
echo "Updating aws profile scripts..."
|
||||
sed -i 's/aws_secret_access_key/aws_secret_key/g' scripts/create-aws-profile.sh
|
||||
sed -i 's/aws_secret_access_key/aws_secret_key/g' scripts/setup-aws-profiles.sh
|
||||
|
||||
# 4. Make awsCredentialProvider test use clearly labeled example values
|
||||
echo "Updating unit test files..."
|
||||
sed -i 's/aws_secret_access_key = default-secret-key/aws_secret_key = example-default-secret-key/g' test/unit/utils/awsCredentialProvider.test.js
|
||||
sed -i 's/aws_secret_access_key = test-secret-key/aws_secret_key = example-test-secret-key/g' test/unit/utils/awsCredentialProvider.test.js
|
||||
|
||||
echo "Updates completed. Running check again..."
|
||||
|
||||
# Check if any sensitive patterns remain (excluding clearly labeled examples)
|
||||
SENSITIVE_FILES=$(grep -r "ghp_\|AKIA\|aws_secret_access_key" --include="*.js" --include="*.sh" --include="*.json" --include="*.md" . | grep -v "EXAMPLE\|example\|REDACTED\|dummy\|\${\|ENV\|process.env\|context.env\|mock\|pattern" || echo "No sensitive data found")
|
||||
|
||||
if [ -n "$SENSITIVE_FILES" ] && [ "$SENSITIVE_FILES" != "No sensitive data found" ]; then
|
||||
echo "⚠️ Some potential sensitive patterns remain:"
|
||||
echo "$SENSITIVE_FILES"
|
||||
echo "Please review manually."
|
||||
else
|
||||
echo "✅ No sensitive patterns found. The repository is ready!"
|
||||
fi
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script to prepare, clean, and set up a new repository
|
||||
|
||||
CURRENT_REPO="/home/jonflatt/n8n/claude-repo"
|
||||
CLEAN_REPO="/tmp/clean-repo"
|
||||
|
||||
echo "=== STEP 1: Preparing clean repository ==="
|
||||
# Run the prepare script
|
||||
bash "$CURRENT_REPO/prepare-clean-repo.sh"
|
||||
|
||||
echo ""
|
||||
echo "=== STEP 2: Fixing credential references ==="
|
||||
# Fix credential references
|
||||
bash "$CURRENT_REPO/fix-credential-references.sh"
|
||||
|
||||
echo ""
|
||||
echo "=== STEP 3: Setting up git repository ==="
|
||||
# Change to the clean repository
|
||||
cd "$CLEAN_REPO" || exit 1
|
||||
|
||||
# Initialize git repository
|
||||
git init
|
||||
|
||||
# Add all files
|
||||
git add .
|
||||
|
||||
# Check if there are any files to commit
|
||||
if ! git diff --cached --quiet; then
|
||||
# Create initial commit
|
||||
git commit -m "Initial commit - Clean repository"
|
||||
|
||||
echo ""
|
||||
echo "=== Repository ready! ==="
|
||||
echo "The clean repository has been created at: $CLEAN_REPO"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Create a new GitHub repository at https://github.com/new"
|
||||
echo "2. Connect this repository to GitHub:"
|
||||
echo " cd $CLEAN_REPO"
|
||||
echo " git remote add origin <your-new-repository-url>"
|
||||
echo " git branch -M main"
|
||||
echo " git push -u origin main"
|
||||
else
|
||||
echo "No files to commit. Something went wrong with the file preparation."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Setting up Claude Code authentication..."
|
||||
|
||||
# Build the setup container
|
||||
docker build -f Dockerfile.setup -t claude-setup .
|
||||
|
||||
# Run it interactively with AWS credentials mounted
|
||||
docker run -it -v $HOME/.aws:/root/.aws:ro claude-setup
|
||||
|
||||
echo ""
|
||||
echo "After completing the authentication in the container:"
|
||||
echo "1. Run 'docker ps -a' to find the container ID"
|
||||
echo "2. Run 'docker cp <container_id>:/root/.claude ./claude-config'"
|
||||
echo "3. Then run './update-production-image.sh'"
|
||||
94
scripts/setup/setup-claude-interactive.sh
Executable file
94
scripts/setup/setup-claude-interactive.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Claude Interactive Authentication Setup Script
|
||||
# This script creates a container for interactive Claude authentication
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
AUTH_OUTPUT_DIR="${CLAUDE_HUB_DIR:-$HOME/.claude-hub}"
|
||||
|
||||
echo "🔧 Claude Interactive Authentication Setup"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Create output directory for authentication state
|
||||
mkdir -p "$AUTH_OUTPUT_DIR"
|
||||
|
||||
echo "📦 Building Claude setup container..."
|
||||
docker build -f "$PROJECT_ROOT/Dockerfile.claude-setup" -t claude-setup:latest "$PROJECT_ROOT"
|
||||
|
||||
echo ""
|
||||
echo "🚀 Starting Claude authentication..."
|
||||
echo ""
|
||||
echo "What happens next:"
|
||||
echo " 1. Claude will open your browser for authentication"
|
||||
echo " 2. Complete the authentication in your browser"
|
||||
echo " 3. Return here when done - the container will exit automatically"
|
||||
echo ""
|
||||
read -p "Press Enter to start authentication..."
|
||||
|
||||
# Run the container with automatic authentication
|
||||
docker run -it --rm \
|
||||
-v "$AUTH_OUTPUT_DIR:/auth-output" \
|
||||
-v "$HOME/.gitconfig:/home/node/.gitconfig:ro" \
|
||||
--name claude-auth-setup \
|
||||
claude-setup:latest --auto
|
||||
|
||||
# Capture the exit code
|
||||
DOCKER_EXIT_CODE=$?
|
||||
|
||||
echo ""
|
||||
echo "📋 Checking authentication output..."
|
||||
|
||||
# First check if docker command failed
|
||||
if [ $DOCKER_EXIT_CODE -ne 0 ]; then
|
||||
echo "❌ Authentication process failed (exit code: $DOCKER_EXIT_CODE)"
|
||||
echo ""
|
||||
echo "Please check the error messages above and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if authentication was successful
|
||||
if [ -f "$AUTH_OUTPUT_DIR/.credentials.json" ]; then
|
||||
# Get file size
|
||||
FILE_SIZE=$(stat -f%z "$AUTH_OUTPUT_DIR/.credentials.json" 2>/dev/null || stat -c%s "$AUTH_OUTPUT_DIR/.credentials.json" 2>/dev/null || echo "0")
|
||||
|
||||
# Check if file has reasonable content (at least 100 bytes for a valid JSON)
|
||||
if [ "$FILE_SIZE" -gt 100 ]; then
|
||||
# Check if file was written recently (within last 5 minutes)
|
||||
if [ "$(find "$AUTH_OUTPUT_DIR/.credentials.json" -mmin -5 2>/dev/null)" ]; then
|
||||
echo "✅ Success! Your Claude authentication is saved."
|
||||
echo ""
|
||||
echo "The webhook service will use this automatically when you run:"
|
||||
echo " docker compose up -d"
|
||||
echo ""
|
||||
exit 0
|
||||
else
|
||||
echo "⚠️ Found old authentication files. The authentication may not have completed."
|
||||
echo "Please run the setup again to refresh your authentication."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Authentication file is too small (${FILE_SIZE} bytes). The authentication did not complete."
|
||||
echo ""
|
||||
echo "Common causes:"
|
||||
echo " - Browser authentication was cancelled"
|
||||
echo " - Network connection issues"
|
||||
echo " - Claude Code subscription not active"
|
||||
echo ""
|
||||
echo "Please run the setup again and complete the browser authentication."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Authentication failed - no credentials were saved."
|
||||
echo ""
|
||||
echo "This can happen if:"
|
||||
echo " - The browser authentication was not completed"
|
||||
echo " - The container exited before authentication finished"
|
||||
echo " - There was an error during the authentication process"
|
||||
echo ""
|
||||
echo "Please run './scripts/setup/setup-claude-interactive.sh' again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup GitHub Actions self-hosted runner for claude-github-webhook
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
RUNNER_DIR="/home/jonflatt/github-actions-runner"
|
||||
RUNNER_VERSION="2.324.0"
|
||||
REPO_URL="https://github.com/intelligence-assist/claude-github-webhook"
|
||||
RUNNER_NAME="claude-webhook-runner"
|
||||
RUNNER_LABELS="self-hosted,linux,x64,claude-webhook"
|
||||
|
||||
echo "🚀 Setting up GitHub Actions self-hosted runner..."
|
||||
|
||||
# Create runner directory
|
||||
mkdir -p "$RUNNER_DIR"
|
||||
cd "$RUNNER_DIR"
|
||||
|
||||
# Download runner if not exists
|
||||
if [ ! -f "actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" ]; then
|
||||
echo "📦 Downloading runner v${RUNNER_VERSION}..."
|
||||
curl -o "actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" -L \
|
||||
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"
|
||||
fi
|
||||
|
||||
# Extract runner
|
||||
echo "📂 Extracting runner..."
|
||||
tar xzf "./actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"
|
||||
|
||||
# Install dependencies if needed
|
||||
echo "🔧 Installing dependencies..."
|
||||
sudo ./bin/installdependencies.sh || true
|
||||
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANT: You need to get a runner registration token from GitHub!"
|
||||
echo ""
|
||||
echo "1. Go to: https://github.com/intelligence-assist/claude-github-webhook/settings/actions/runners/new"
|
||||
echo "2. Copy the registration token"
|
||||
echo "3. Run the configuration command below with your token:"
|
||||
echo ""
|
||||
echo "cd $RUNNER_DIR"
|
||||
echo "./config.sh --url $REPO_URL --token YOUR_TOKEN_HERE --name $RUNNER_NAME --labels $RUNNER_LABELS --unattended --replace"
|
||||
echo ""
|
||||
echo "4. After configuration, install as a service:"
|
||||
echo "sudo ./svc.sh install"
|
||||
echo "sudo ./svc.sh start"
|
||||
echo ""
|
||||
echo "5. Check status:"
|
||||
echo "sudo ./svc.sh status"
|
||||
echo ""
|
||||
|
||||
# Create systemd service file for the runner
|
||||
cat > "$RUNNER_DIR/actions.runner.service" << 'EOF'
|
||||
[Unit]
|
||||
Description=GitHub Actions Runner (claude-webhook-runner)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=jonflatt
|
||||
WorkingDirectory=/home/jonflatt/github-actions-runner
|
||||
ExecStart=/home/jonflatt/github-actions-runner/run.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
KillMode=process
|
||||
KillSignal=SIGTERM
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=github-runner
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/jonflatt/github-actions-runner
|
||||
ReadWritePaths=/home/jonflatt/n8n/claude-repo
|
||||
ReadWritePaths=/var/run/docker.sock
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "📄 Systemd service file created at: $RUNNER_DIR/actions.runner.service"
|
||||
echo ""
|
||||
echo "Alternative: Use systemd directly instead of ./svc.sh:"
|
||||
echo "sudo cp $RUNNER_DIR/actions.runner.service /etc/systemd/system/github-runner-claude.service"
|
||||
echo "sudo systemctl daemon-reload"
|
||||
echo "sudo systemctl enable github-runner-claude"
|
||||
echo "sudo systemctl start github-runner-claude"
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script to set up the new clean repository
|
||||
|
||||
CLEAN_REPO="/tmp/clean-repo"
|
||||
|
||||
# Change to the clean repository
|
||||
cd "$CLEAN_REPO" || exit 1
|
||||
echo "Changed to directory: $(pwd)"
|
||||
|
||||
# Initialize git repository
|
||||
echo "Initializing git repository..."
|
||||
git init
|
||||
|
||||
# Configure git if needed (optional)
|
||||
# git config user.name "Your Name"
|
||||
# git config user.email "your.email@example.com"
|
||||
|
||||
# Add all files
|
||||
echo "Adding files to git..."
|
||||
git add .
|
||||
|
||||
# First checking for any remaining sensitive data
|
||||
echo "Checking for potential sensitive data..."
|
||||
SENSITIVE_FILES=$(grep -r "ghp_\|AKIA\|aws_secret\|github_token" --include="*.js" --include="*.sh" --include="*.json" --include="*.md" . | grep -v "EXAMPLE\|REDACTED\|dummy\|\${\|ENV\|process.env\|context.env\|mock" || echo "No sensitive data found")
|
||||
|
||||
if [ -n "$SENSITIVE_FILES" ]; then
|
||||
echo "⚠️ Potential sensitive data found:"
|
||||
echo "$SENSITIVE_FILES"
|
||||
echo ""
|
||||
echo "Please review the above files and remove any real credentials before continuing."
|
||||
echo "After fixing, run this script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Commit the code
|
||||
echo "Creating initial commit..."
|
||||
git commit -m "Initial commit - Clean repository" || exit 1
|
||||
|
||||
echo ""
|
||||
echo "✅ Repository setup complete!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Create a new GitHub repository at https://github.com/new"
|
||||
echo "2. Connect and push this repository with:"
|
||||
echo " git remote add origin <your-new-repository-url>"
|
||||
echo " git branch -M main"
|
||||
echo " git push -u origin main"
|
||||
echo ""
|
||||
echo "Important: The repository is ready at $CLEAN_REPO"
|
||||
91
scripts/setup/test-claude-auth.sh
Executable file
91
scripts/setup/test-claude-auth.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Test captured Claude authentication
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
AUTH_OUTPUT_DIR="${CLAUDE_HUB_DIR:-$HOME/.claude-hub}"
|
||||
|
||||
echo "🧪 Testing Claude Authentication"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
if [ ! -d "$AUTH_OUTPUT_DIR" ]; then
|
||||
echo "❌ Authentication directory not found: $AUTH_OUTPUT_DIR"
|
||||
echo " Run ./scripts/setup/setup-claude-interactive.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📁 Authentication files found:"
|
||||
find "$AUTH_OUTPUT_DIR" -type f | head -20
|
||||
echo ""
|
||||
|
||||
echo "🔍 Testing authentication with Claude CLI..."
|
||||
echo ""
|
||||
|
||||
# Test Claude version
|
||||
echo "1. Testing Claude CLI version..."
|
||||
docker run --rm \
|
||||
-v "$AUTH_OUTPUT_DIR:/home/node/.claude:ro" \
|
||||
claude-setup:latest \
|
||||
sudo -u node -E env HOME=/home/node PATH=/usr/local/share/npm-global/bin:$PATH \
|
||||
/usr/local/share/npm-global/bin/claude --version
|
||||
|
||||
echo ""
|
||||
|
||||
# Test Claude status (might fail due to TTY requirements)
|
||||
echo "2. Testing Claude status..."
|
||||
docker run --rm \
|
||||
-v "$AUTH_OUTPUT_DIR:/home/node/.claude:ro" \
|
||||
claude-setup:latest \
|
||||
timeout 5 sudo -u node -E env HOME=/home/node PATH=/usr/local/share/npm-global/bin:$PATH \
|
||||
/usr/local/share/npm-global/bin/claude status 2>&1 || echo "Status command failed (expected due to TTY requirements)"
|
||||
|
||||
echo ""
|
||||
|
||||
# Test Claude with a simple print command
|
||||
echo "3. Testing Claude with simple command..."
|
||||
docker run --rm \
|
||||
-v "$AUTH_OUTPUT_DIR:/home/node/.claude:ro" \
|
||||
claude-setup:latest \
|
||||
timeout 10 sudo -u node -E env HOME=/home/node PATH=/usr/local/share/npm-global/bin:$PATH \
|
||||
/usr/local/share/npm-global/bin/claude --print "Hello, testing authentication" 2>&1 || echo "Print command failed"
|
||||
|
||||
echo ""
|
||||
echo "🔍 Authentication file analysis:"
|
||||
echo "================================"
|
||||
|
||||
# Check for key authentication files
|
||||
if [ -f "$AUTH_OUTPUT_DIR/.credentials.json" ]; then
|
||||
echo "✅ .credentials.json found ($(wc -c < "$AUTH_OUTPUT_DIR/.credentials.json") bytes)"
|
||||
else
|
||||
echo "❌ .credentials.json not found"
|
||||
fi
|
||||
|
||||
if [ -f "$AUTH_OUTPUT_DIR/settings.local.json" ]; then
|
||||
echo "✅ settings.local.json found"
|
||||
echo " Contents: $(head -1 "$AUTH_OUTPUT_DIR/settings.local.json")"
|
||||
else
|
||||
echo "❌ settings.local.json not found"
|
||||
fi
|
||||
|
||||
if [ -d "$AUTH_OUTPUT_DIR/statsig" ]; then
|
||||
echo "✅ statsig directory found ($(ls -1 "$AUTH_OUTPUT_DIR/statsig" | wc -l) files)"
|
||||
else
|
||||
echo "❌ statsig directory not found"
|
||||
fi
|
||||
|
||||
# Look for SQLite databases
|
||||
DB_FILES=$(find "$AUTH_OUTPUT_DIR" -name "*.db" 2>/dev/null | wc -l)
|
||||
if [ "$DB_FILES" -gt 0 ]; then
|
||||
echo "✅ Found $DB_FILES SQLite database files"
|
||||
find "$AUTH_OUTPUT_DIR" -name "*.db" | head -5
|
||||
else
|
||||
echo "❌ No SQLite database files found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "💡 Next steps:"
|
||||
echo " If authentication tests pass, copy to your main Claude directory:"
|
||||
echo " cp -r $AUTH_OUTPUT_DIR/* ~/.claude/"
|
||||
echo " Or update your webhook service to use this authentication directory"
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Benchmark script for measuring spin-up times
|
||||
set -e
|
||||
|
||||
BENCHMARK_RUNS=${1:-3}
|
||||
COMPOSE_FILE=${2:-docker-compose.yml}
|
||||
|
||||
echo "Benchmarking startup time with $COMPOSE_FILE (${BENCHMARK_RUNS} runs)"
|
||||
echo "=============================================="
|
||||
|
||||
TOTAL_TIME=0
|
||||
RESULTS=()
|
||||
|
||||
for i in $(seq 1 $BENCHMARK_RUNS); do
|
||||
echo "Run $i/$BENCHMARK_RUNS:"
|
||||
|
||||
# Ensure clean state
|
||||
docker compose -f $COMPOSE_FILE down >/dev/null 2>&1 || true
|
||||
docker system prune -f >/dev/null 2>&1 || true
|
||||
|
||||
# Start timing
|
||||
START_TIME=$(date +%s%3N)
|
||||
|
||||
# Start service
|
||||
docker compose -f $COMPOSE_FILE up -d >/dev/null 2>&1
|
||||
|
||||
# Wait for health check to pass
|
||||
echo -n " Waiting for service to be ready."
|
||||
while true; do
|
||||
if curl -s -f http://localhost:8082/health >/dev/null 2>&1; then
|
||||
READY_TIME=$(date +%s%3N)
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
ELAPSED=$((READY_TIME - START_TIME))
|
||||
TOTAL_TIME=$((TOTAL_TIME + ELAPSED))
|
||||
RESULTS+=($ELAPSED)
|
||||
|
||||
echo " Ready! (${ELAPSED}ms)"
|
||||
|
||||
# Get detailed startup metrics
|
||||
METRICS=$(curl -s http://localhost:8082/health | jq -r '.startup.totalElapsed // "N/A"')
|
||||
echo " App startup time: ${METRICS}ms"
|
||||
|
||||
# Clean up
|
||||
docker compose -f $COMPOSE_FILE down >/dev/null 2>&1
|
||||
|
||||
# Brief pause between runs
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Results Summary:"
|
||||
echo "=============================================="
|
||||
|
||||
AVERAGE=$((TOTAL_TIME / BENCHMARK_RUNS))
|
||||
echo "Average startup time: ${AVERAGE}ms"
|
||||
|
||||
# Calculate min/max
|
||||
MIN=${RESULTS[0]}
|
||||
MAX=${RESULTS[0]}
|
||||
for time in "${RESULTS[@]}"; do
|
||||
[ $time -lt $MIN ] && MIN=$time
|
||||
[ $time -gt $MAX ] && MAX=$time
|
||||
done
|
||||
|
||||
echo "Fastest: ${MIN}ms"
|
||||
echo "Slowest: ${MAX}ms"
|
||||
echo "Individual results: ${RESULTS[*]}"
|
||||
|
||||
# Save results to file
|
||||
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
|
||||
RESULTS_FILE="benchmark_results_${TIMESTAMP}.json"
|
||||
|
||||
cat > $RESULTS_FILE << EOF
|
||||
{
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"compose_file": "$COMPOSE_FILE",
|
||||
"runs": $BENCHMARK_RUNS,
|
||||
"results_ms": [$(IFS=,; echo "${RESULTS[*]}")],
|
||||
"average_ms": $AVERAGE,
|
||||
"min_ms": $MIN,
|
||||
"max_ms": $MAX
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "Results saved to: $RESULTS_FILE"
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test container with a volume mount for output
|
||||
OUTPUT_DIR="/tmp/claude-output"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/output.txt"
|
||||
|
||||
echo "Docker Container Volume Test"
|
||||
echo "=========================="
|
||||
|
||||
# Ensure output directory exists and is empty
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rm -f "$OUTPUT_FILE"
|
||||
|
||||
# Run container with volume mount for output
|
||||
docker run --rm \
|
||||
-v "$OUTPUT_DIR:/output" \
|
||||
claudecode:latest \
|
||||
bash -c "echo 'Hello from container' > /output/output.txt && echo 'Command executed successfully.'"
|
||||
|
||||
# Check if output file was created
|
||||
echo
|
||||
echo "Checking for output file: $OUTPUT_FILE"
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
echo "Output file created. Contents:"
|
||||
cat "$OUTPUT_FILE"
|
||||
else
|
||||
echo "No output file was created."
|
||||
fi
|
||||
File diff suppressed because it is too large
Load Diff
1711
src/controllers/githubController.ts
Normal file
1711
src/controllers/githubController.ts
Normal file
File diff suppressed because it is too large
Load Diff
142
src/index.js
142
src/index.js
@@ -1,142 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const { createLogger } = require('./utils/logger');
|
||||
const { StartupMetrics } = require('./utils/startup-metrics');
|
||||
const githubRoutes = require('./routes/github');
|
||||
const claudeRoutes = require('./routes/claude');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3003;
|
||||
const appLogger = createLogger('app');
|
||||
const startupMetrics = new StartupMetrics();
|
||||
|
||||
// Record initial milestones
|
||||
startupMetrics.recordMilestone('env_loaded', 'Environment variables loaded');
|
||||
startupMetrics.recordMilestone('express_initialized', 'Express app initialized');
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
appLogger.info(
|
||||
{
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
statusCode: res.statusCode,
|
||||
responseTime: `${responseTime}ms`
|
||||
},
|
||||
`${req.method} ${req.url}`
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(startupMetrics.metricsMiddleware());
|
||||
|
||||
app.use(
|
||||
bodyParser.json({
|
||||
verify: (req, res, buf) => {
|
||||
// Store the raw body buffer for webhook signature verification
|
||||
req.rawBody = buf;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
startupMetrics.recordMilestone('middleware_configured', 'Express middleware configured');
|
||||
|
||||
// Routes
|
||||
app.use('/api/webhooks/github', githubRoutes);
|
||||
app.use('/api/claude', claudeRoutes);
|
||||
|
||||
startupMetrics.recordMilestone('routes_configured', 'API routes configured');
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', async (req, res) => {
|
||||
const healthCheckStart = Date.now();
|
||||
|
||||
const checks = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
startup: req.startupMetrics,
|
||||
docker: {
|
||||
available: false,
|
||||
error: null,
|
||||
checkTime: null
|
||||
},
|
||||
claudeCodeImage: {
|
||||
available: false,
|
||||
error: null,
|
||||
checkTime: null
|
||||
}
|
||||
};
|
||||
|
||||
// Check Docker availability
|
||||
const dockerCheckStart = Date.now();
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
execSync('docker ps', { stdio: 'ignore' });
|
||||
checks.docker.available = true;
|
||||
} catch (error) {
|
||||
checks.docker.error = error.message;
|
||||
}
|
||||
checks.docker.checkTime = Date.now() - dockerCheckStart;
|
||||
|
||||
// Check Claude Code runner image
|
||||
const imageCheckStart = Date.now();
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
execSync('docker image inspect claude-code-runner:latest', { stdio: 'ignore' });
|
||||
checks.claudeCodeImage.available = true;
|
||||
} catch {
|
||||
checks.claudeCodeImage.error = 'Image not found';
|
||||
}
|
||||
checks.claudeCodeImage.checkTime = Date.now() - imageCheckStart;
|
||||
|
||||
// Set overall status
|
||||
if (!checks.docker.available || !checks.claudeCodeImage.available) {
|
||||
checks.status = 'degraded';
|
||||
}
|
||||
|
||||
checks.healthCheckDuration = Date.now() - healthCheckStart;
|
||||
res.status(200).json(checks);
|
||||
});
|
||||
|
||||
// Test endpoint for CF tunnel
|
||||
app.get('/api/test-tunnel', (req, res) => {
|
||||
appLogger.info('Test tunnel endpoint hit');
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
message: 'CF tunnel is working!',
|
||||
timestamp: new Date().toISOString(),
|
||||
headers: req.headers,
|
||||
ip: req.ip || req.connection.remoteAddress
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, _next) => {
|
||||
appLogger.error(
|
||||
{
|
||||
err: {
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
},
|
||||
method: req.method,
|
||||
url: req.url
|
||||
},
|
||||
'Request error'
|
||||
);
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
startupMetrics.recordMilestone('server_listening', `Server listening on port ${PORT}`);
|
||||
const totalStartupTime = startupMetrics.markReady();
|
||||
appLogger.info(`Server running on port ${PORT} (startup took ${totalStartupTime}ms)`);
|
||||
});
|
||||
195
src/index.ts
Normal file
195
src/index.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { createLogger } from './utils/logger';
|
||||
import { StartupMetrics } from './utils/startup-metrics';
|
||||
import githubRoutes from './routes/github';
|
||||
import claudeRoutes from './routes/claude';
|
||||
import type { WebhookRequest, HealthCheckResponse, ErrorResponse } from './types/express';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Configure trust proxy setting based on environment
|
||||
// Set TRUST_PROXY=true when running behind reverse proxies (nginx, cloudflare, etc.)
|
||||
const trustProxy = process.env['TRUST_PROXY'] === 'true';
|
||||
if (trustProxy) {
|
||||
app.set('trust proxy', true);
|
||||
}
|
||||
|
||||
const PORT = parseInt(process.env['PORT'] ?? '3002', 10);
|
||||
const appLogger = createLogger('app');
|
||||
const startupMetrics = new StartupMetrics();
|
||||
|
||||
// Record initial milestones
|
||||
startupMetrics.recordMilestone('env_loaded', 'Environment variables loaded');
|
||||
startupMetrics.recordMilestone('express_initialized', 'Express app initialized');
|
||||
|
||||
// Rate limiting configuration
|
||||
// When behind a proxy, we need to properly handle client IP detection
|
||||
const rateLimitConfig = {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
// Skip validation when behind proxy to avoid startup errors
|
||||
validate: trustProxy ? false : undefined
|
||||
};
|
||||
|
||||
const generalRateLimit = rateLimit({
|
||||
...rateLimitConfig,
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: {
|
||||
error: 'Too many requests',
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
}
|
||||
});
|
||||
|
||||
const webhookRateLimit = rateLimit({
|
||||
...rateLimitConfig,
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 50, // Limit each IP to 50 webhook requests per 5 minutes
|
||||
message: {
|
||||
error: 'Too many webhook requests',
|
||||
message: 'Too many webhook requests from this IP, please try again later.'
|
||||
},
|
||||
skip: _req => {
|
||||
// Skip rate limiting in test environment
|
||||
return process.env['NODE_ENV'] === 'test';
|
||||
}
|
||||
});
|
||||
|
||||
// Apply rate limiting
|
||||
app.use('/api/webhooks', webhookRateLimit);
|
||||
app.use(generalRateLimit);
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const responseTime = Date.now() - startTime;
|
||||
appLogger.info(
|
||||
{
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
statusCode: res.statusCode,
|
||||
responseTime: `${responseTime}ms`
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
`${req.method?.replace(/[\r\n\t]/g, '_') || 'UNKNOWN'} ${req.url?.replace(/[\r\n\t]/g, '_') || '/unknown'}`
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(startupMetrics.metricsMiddleware());
|
||||
|
||||
app.use(
|
||||
bodyParser.json({
|
||||
verify: (req: WebhookRequest, _res, buf) => {
|
||||
// Store the raw body buffer for webhook signature verification
|
||||
req.rawBody = buf;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
startupMetrics.recordMilestone('middleware_configured', 'Express middleware configured');
|
||||
|
||||
// Routes
|
||||
app.use('/api/webhooks/github', githubRoutes);
|
||||
app.use('/api/claude', claudeRoutes);
|
||||
|
||||
startupMetrics.recordMilestone('routes_configured', 'API routes configured');
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req: WebhookRequest, res: express.Response<HealthCheckResponse>) => {
|
||||
const healthCheckStart = Date.now();
|
||||
|
||||
const checks: HealthCheckResponse = {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
startup: req.startupMetrics,
|
||||
docker: {
|
||||
available: false,
|
||||
error: null,
|
||||
checkTime: null
|
||||
},
|
||||
claudeCodeImage: {
|
||||
available: false,
|
||||
error: null,
|
||||
checkTime: null
|
||||
}
|
||||
};
|
||||
|
||||
// Check Docker availability
|
||||
const dockerCheckStart = Date.now();
|
||||
try {
|
||||
execSync('docker ps', { stdio: 'ignore' });
|
||||
checks.docker.available = true;
|
||||
} catch (error) {
|
||||
checks.docker.error = (error as Error).message;
|
||||
}
|
||||
checks.docker.checkTime = Date.now() - dockerCheckStart;
|
||||
|
||||
// Check Claude Code runner image
|
||||
const imageCheckStart = Date.now();
|
||||
const dockerImageName = process.env['CLAUDE_CONTAINER_IMAGE'] ?? 'claudecode:latest';
|
||||
try {
|
||||
execSync(`docker image inspect ${dockerImageName}`, { stdio: 'ignore' });
|
||||
checks.claudeCodeImage.available = true;
|
||||
} catch {
|
||||
checks.claudeCodeImage.error = 'Image not found';
|
||||
}
|
||||
checks.claudeCodeImage.checkTime = Date.now() - imageCheckStart;
|
||||
|
||||
// Set overall status
|
||||
if (!checks.docker.available || !checks.claudeCodeImage.available) {
|
||||
checks.status = 'degraded';
|
||||
}
|
||||
|
||||
checks.healthCheckDuration = Date.now() - healthCheckStart;
|
||||
res.status(200).json(checks);
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use(
|
||||
(
|
||||
err: Error,
|
||||
req: express.Request,
|
||||
res: express.Response<ErrorResponse>,
|
||||
_next: express.NextFunction
|
||||
) => {
|
||||
appLogger.error(
|
||||
{
|
||||
err: {
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
},
|
||||
method: req.method,
|
||||
url: req.url
|
||||
},
|
||||
'Request error'
|
||||
);
|
||||
|
||||
// Handle JSON parsing errors
|
||||
if (err instanceof SyntaxError && 'body' in err) {
|
||||
res.status(400).json({ error: 'Invalid JSON' });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Only start the server if this is the main module (not being imported for testing)
|
||||
if (require.main === module) {
|
||||
app.listen(PORT, () => {
|
||||
startupMetrics.recordMilestone('server_listening', `Server listening on port ${PORT}`);
|
||||
const totalStartupTime = startupMetrics.markReady();
|
||||
appLogger.info(`Server running on port ${PORT} (startup took ${totalStartupTime}ms)`);
|
||||
});
|
||||
}
|
||||
|
||||
export default app;
|
||||
@@ -1,21 +1,31 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const claudeService = require('../services/claudeService');
|
||||
const { createLogger } = require('../utils/logger');
|
||||
import express from 'express';
|
||||
import { processCommand } from '../services/claudeService';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import type { ClaudeAPIHandler } from '../types/express';
|
||||
|
||||
const router = express.Router();
|
||||
const logger = createLogger('claudeRoutes');
|
||||
|
||||
/**
|
||||
* Direct endpoint for Claude processing
|
||||
* Allows calling Claude without GitHub webhook integration
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
const handleClaudeRequest: ClaudeAPIHandler = async (req, res) => {
|
||||
logger.info({ request: req.body }, 'Received direct Claude request');
|
||||
try {
|
||||
const { repoFullName, repository, command, authToken, useContainer = false } = req.body;
|
||||
const {
|
||||
repoFullName,
|
||||
repository,
|
||||
command,
|
||||
authToken,
|
||||
useContainer = false,
|
||||
issueNumber,
|
||||
isPullRequest = false,
|
||||
branchName
|
||||
} = req.body;
|
||||
|
||||
// Handle both repoFullName and repository parameters
|
||||
const repoName = repoFullName || repository;
|
||||
const repoName = repoFullName ?? repository;
|
||||
|
||||
// Validate required parameters
|
||||
if (!repoName) {
|
||||
@@ -29,8 +39,8 @@ router.post('/', async (req, res) => {
|
||||
}
|
||||
|
||||
// Validate authentication if enabled
|
||||
if (process.env.CLAUDE_API_AUTH_REQUIRED === '1') {
|
||||
if (!authToken || authToken !== process.env.CLAUDE_API_AUTH_TOKEN) {
|
||||
if (process.env['CLAUDE_API_AUTH_REQUIRED'] === '1') {
|
||||
if (!authToken || authToken !== process.env['CLAUDE_API_AUTH_TOKEN']) {
|
||||
logger.warn('Invalid authentication token');
|
||||
return res.status(401).json({ error: 'Invalid authentication token' });
|
||||
}
|
||||
@@ -40,20 +50,22 @@ router.post('/', async (req, res) => {
|
||||
{
|
||||
repo: repoName,
|
||||
commandLength: command.length,
|
||||
useContainer
|
||||
useContainer,
|
||||
issueNumber,
|
||||
isPullRequest
|
||||
},
|
||||
'Processing direct Claude command'
|
||||
);
|
||||
|
||||
// Process the command with Claude
|
||||
let claudeResponse;
|
||||
let claudeResponse: string;
|
||||
try {
|
||||
claudeResponse = await claudeService.processCommand({
|
||||
claudeResponse = await processCommand({
|
||||
repoFullName: repoName,
|
||||
issueNumber: null, // No issue number for direct calls
|
||||
issueNumber: issueNumber ?? null,
|
||||
command,
|
||||
isPullRequest: false,
|
||||
branchName: null
|
||||
isPullRequest,
|
||||
branchName: branchName ?? null
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
@@ -70,8 +82,11 @@ router.post('/', async (req, res) => {
|
||||
'No output received from Claude container. This is a placeholder response.';
|
||||
}
|
||||
} catch (processingError) {
|
||||
logger.error({ error: processingError }, 'Error during Claude processing');
|
||||
claudeResponse = `Error: ${processingError.message}`;
|
||||
const err = processingError as Error;
|
||||
logger.error({ error: err }, 'Error during Claude processing');
|
||||
// When Claude processing fails, we still return 200 but with the error message
|
||||
// This allows the webhook to complete successfully even if Claude had issues
|
||||
claudeResponse = `Error: ${err.message}`;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -86,11 +101,12 @@ router.post('/', async (req, res) => {
|
||||
response: claudeResponse
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
}
|
||||
},
|
||||
'Error processing direct Claude command'
|
||||
@@ -98,9 +114,11 @@ router.post('/', async (req, res) => {
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'Failed to process command',
|
||||
message: error.message
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
router.post('/', handleClaudeRequest as express.RequestHandler);
|
||||
|
||||
export default router;
|
||||
@@ -1,8 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const githubController = require('../controllers/githubController');
|
||||
|
||||
// GitHub webhook endpoint
|
||||
router.post('/', githubController.handleWebhook);
|
||||
|
||||
module.exports = router;
|
||||
9
src/routes/github.ts
Normal file
9
src/routes/github.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import express from 'express';
|
||||
import { handleWebhook } from '../controllers/githubController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GitHub webhook endpoint
|
||||
router.post('/', handleWebhook as express.RequestHandler);
|
||||
|
||||
export default router;
|
||||
@@ -1,573 +0,0 @@
|
||||
const { execFileSync } = require('child_process');
|
||||
// Use sync methods for file operations that need to be synchronous
|
||||
const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
// const os = require('os');
|
||||
const { createLogger } = require('../utils/logger');
|
||||
// const awsCredentialProvider = require('../utils/awsCredentialProvider');
|
||||
const { sanitizeBotMentions } = require('../utils/sanitize');
|
||||
const secureCredentials = require('../utils/secureCredentials');
|
||||
|
||||
const logger = createLogger('claudeService');
|
||||
|
||||
// Get bot username from environment variables - required
|
||||
const BOT_USERNAME = process.env.BOT_USERNAME;
|
||||
|
||||
// Validate bot username is set
|
||||
if (!BOT_USERNAME) {
|
||||
logger.error(
|
||||
'BOT_USERNAME environment variable is not set in claudeService. This is required to prevent infinite loops.'
|
||||
);
|
||||
throw new Error('BOT_USERNAME environment variable is required');
|
||||
}
|
||||
|
||||
// Using the shared sanitization utility from utils/sanitize.js
|
||||
|
||||
/**
|
||||
* Processes a command using Claude Code CLI
|
||||
*
|
||||
* @param {Object} options - The options for processing the command
|
||||
* @param {string} options.repoFullName - The full name of the repository (owner/repo)
|
||||
* @param {number|null} options.issueNumber - The issue number (can be null for direct API calls)
|
||||
* @param {string} options.command - The command to process with Claude
|
||||
* @param {boolean} [options.isPullRequest=false] - Whether this is a pull request
|
||||
* @param {string} [options.branchName] - The branch name for pull requests
|
||||
* @returns {Promise<string>} - Claude's response
|
||||
*/
|
||||
async function processCommand({
|
||||
repoFullName,
|
||||
issueNumber,
|
||||
command,
|
||||
isPullRequest = false,
|
||||
branchName = null
|
||||
}) {
|
||||
try {
|
||||
logger.info(
|
||||
{
|
||||
repo: repoFullName,
|
||||
issue: issueNumber,
|
||||
isPullRequest,
|
||||
branchName,
|
||||
commandLength: command.length
|
||||
},
|
||||
'Processing command with Claude'
|
||||
);
|
||||
|
||||
const githubToken = secureCredentials.get('GITHUB_TOKEN');
|
||||
|
||||
// In test mode, skip execution and return a mock response
|
||||
if (process.env.NODE_ENV === 'test' || !githubToken || !githubToken.includes('ghp_')) {
|
||||
logger.info(
|
||||
{
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'TEST MODE: Skipping Claude execution'
|
||||
);
|
||||
|
||||
// Create a test response and sanitize it
|
||||
const testResponse = `Hello! I'm Claude responding to your request.
|
||||
|
||||
Since this is a test environment, I'm providing a simulated response. In production, I would:
|
||||
1. Clone the repository ${repoFullName}
|
||||
2. ${isPullRequest ? `Checkout PR branch: ${branchName}` : 'Use the main branch'}
|
||||
3. Analyze the codebase and execute: "${command}"
|
||||
4. Use GitHub CLI to interact with issues, PRs, and comments
|
||||
|
||||
For real functionality, please configure valid GitHub and Claude API tokens.`;
|
||||
|
||||
// Always sanitize responses, even in test mode
|
||||
return sanitizeBotMentions(testResponse);
|
||||
}
|
||||
|
||||
// Build Docker image if it doesn't exist
|
||||
const dockerImageName = process.env.CLAUDE_CONTAINER_IMAGE || 'claude-code-runner:latest';
|
||||
try {
|
||||
execFileSync('docker', ['inspect', dockerImageName], { stdio: 'ignore' });
|
||||
logger.info({ dockerImageName }, 'Docker image already exists');
|
||||
} catch (_e) {
|
||||
logger.info({ dockerImageName }, 'Building Docker image for Claude Code runner');
|
||||
execFileSync('docker', ['build', '-f', 'Dockerfile.claudecode', '-t', dockerImageName, '.'], {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
}
|
||||
|
||||
// Create unique container name (sanitized to prevent command injection)
|
||||
const sanitizedRepoName = repoFullName.replace(/[^a-zA-Z0-9\-_]/g, '-');
|
||||
const containerName = `claude-${sanitizedRepoName}-${Date.now()}`;
|
||||
|
||||
// Create the full prompt with context and instructions
|
||||
const fullPrompt = `You are Claude, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'} via the ${BOT_USERNAME} webhook.
|
||||
|
||||
**Context:**
|
||||
- Repository: ${repoFullName}
|
||||
- ${isPullRequest ? 'Pull Request' : 'Issue'} Number: #${issueNumber}
|
||||
- Current Branch: ${branchName || 'main'}
|
||||
- Running in: Unattended mode
|
||||
|
||||
**Important Instructions:**
|
||||
1. You have full GitHub CLI access via the 'gh' command
|
||||
2. When writing code:
|
||||
- Always create a feature branch for new work
|
||||
- Make commits with descriptive messages
|
||||
- Push your work to the remote repository
|
||||
- Run all tests and ensure they pass
|
||||
- Fix any linting or type errors
|
||||
- Create a pull request if appropriate
|
||||
3. Iterate until the task is complete - don't stop at partial solutions
|
||||
4. Always check in your work by pushing to the remote before finishing
|
||||
5. Use 'gh issue comment' or 'gh pr comment' to provide updates on your progress
|
||||
6. If you encounter errors, debug and fix them before completing
|
||||
7. **IMPORTANT - Markdown Formatting:**
|
||||
- When your response contains markdown (like headers, lists, code blocks), return it as properly formatted markdown
|
||||
- Do NOT escape or encode special characters like newlines (\\n) or quotes
|
||||
- Return clean, human-readable markdown that GitHub will render correctly
|
||||
- Your response should look like normal markdown text, not escaped strings
|
||||
8. **Request Acknowledgment:**
|
||||
- For larger or complex tasks that will take significant time, first acknowledge the request
|
||||
- Post a brief comment like "I understand. Working on [task description]..." before starting
|
||||
- Use 'gh issue comment' or 'gh pr comment' to post this acknowledgment immediately
|
||||
- This lets the user know their request was received and is being processed
|
||||
|
||||
**User Request:**
|
||||
${command}
|
||||
|
||||
Please complete this task fully and autonomously.`;
|
||||
|
||||
// Prepare environment variables for the container
|
||||
const envVars = {
|
||||
REPO_FULL_NAME: repoFullName,
|
||||
ISSUE_NUMBER: issueNumber || '',
|
||||
IS_PULL_REQUEST: isPullRequest ? 'true' : 'false',
|
||||
BRANCH_NAME: branchName || '',
|
||||
COMMAND: fullPrompt,
|
||||
GITHUB_TOKEN: githubToken,
|
||||
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY')
|
||||
};
|
||||
|
||||
// Build docker run command - properly escape values for shell
|
||||
Object.entries(envVars)
|
||||
.filter(([_, value]) => value !== undefined && value !== '')
|
||||
.map(([key, value]) => {
|
||||
// Convert to string and escape shell special characters in the value
|
||||
const stringValue = String(value);
|
||||
// Write complex values to files for safer handling
|
||||
if (key === 'COMMAND' && stringValue.length > 500) {
|
||||
const crypto = require('crypto');
|
||||
const randomSuffix = crypto.randomBytes(16).toString('hex');
|
||||
const tmpFile = `/tmp/claude-command-${Date.now()}-${randomSuffix}.txt`;
|
||||
fsSync.writeFileSync(tmpFile, stringValue, { mode: 0o600 }); // Secure file permissions
|
||||
return `-e ${key}="$(cat ${tmpFile})"`;
|
||||
}
|
||||
// Escape for shell with double quotes (more reliable than single quotes)
|
||||
const escapedValue = stringValue.replace(/["\\$`!]/g, '\\$&');
|
||||
return `-e ${key}="${escapedValue}"`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
// Run the container
|
||||
logger.info(
|
||||
{
|
||||
containerName,
|
||||
repo: repoFullName,
|
||||
isPullRequest,
|
||||
branch: branchName
|
||||
},
|
||||
'Starting Claude Code container'
|
||||
);
|
||||
|
||||
// Build docker run command as an array to prevent command injection
|
||||
const dockerArgs = [
|
||||
'run',
|
||||
'--rm'
|
||||
];
|
||||
|
||||
// Apply container security constraints based on environment variables
|
||||
if (process.env.CLAUDE_CONTAINER_PRIVILEGED === 'true') {
|
||||
dockerArgs.push('--privileged');
|
||||
} else {
|
||||
// Apply only necessary capabilities instead of privileged mode
|
||||
const requiredCapabilities = [
|
||||
'NET_ADMIN', // Required for firewall setup
|
||||
'SYS_ADMIN' // Required for certain filesystem operations
|
||||
];
|
||||
|
||||
// Add optional capabilities
|
||||
const optionalCapabilities = {
|
||||
'NET_RAW': process.env.CLAUDE_CONTAINER_CAP_NET_RAW === 'true',
|
||||
'SYS_TIME': process.env.CLAUDE_CONTAINER_CAP_SYS_TIME === 'true',
|
||||
'DAC_OVERRIDE': process.env.CLAUDE_CONTAINER_CAP_DAC_OVERRIDE === 'true',
|
||||
'AUDIT_WRITE': process.env.CLAUDE_CONTAINER_CAP_AUDIT_WRITE === 'true'
|
||||
};
|
||||
|
||||
// Add required capabilities
|
||||
requiredCapabilities.forEach(cap => {
|
||||
dockerArgs.push(`--cap-add=${cap}`);
|
||||
});
|
||||
|
||||
// Add optional capabilities if enabled
|
||||
Object.entries(optionalCapabilities).forEach(([cap, enabled]) => {
|
||||
if (enabled) {
|
||||
dockerArgs.push(`--cap-add=${cap}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Add resource limits
|
||||
dockerArgs.push(
|
||||
'--memory', process.env.CLAUDE_CONTAINER_MEMORY_LIMIT || '2g',
|
||||
'--cpu-shares', process.env.CLAUDE_CONTAINER_CPU_SHARES || '1024',
|
||||
'--pids-limit', process.env.CLAUDE_CONTAINER_PIDS_LIMIT || '256'
|
||||
);
|
||||
}
|
||||
|
||||
// Add container name
|
||||
dockerArgs.push('--name', containerName);
|
||||
|
||||
// Add environment variables as separate arguments
|
||||
Object.entries(envVars)
|
||||
.filter(([_, value]) => value !== undefined && value !== '')
|
||||
.forEach(([key, value]) => {
|
||||
// Write complex values to files for safer handling
|
||||
if (key === 'COMMAND' && String(value).length > 500) {
|
||||
const crypto = require('crypto');
|
||||
const randomSuffix = crypto.randomBytes(16).toString('hex');
|
||||
const tmpFile = `/tmp/claude-command-${Date.now()}-${randomSuffix}.txt`;
|
||||
fsSync.writeFileSync(tmpFile, String(value), { mode: 0o600 }); // Secure file permissions
|
||||
dockerArgs.push('-e', `${key}=@${tmpFile}`);
|
||||
} else {
|
||||
dockerArgs.push('-e', `${key}=${String(value)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the image name as the final argument
|
||||
dockerArgs.push(dockerImageName);
|
||||
|
||||
// Create sanitized version for logging (remove sensitive values)
|
||||
const sanitizedArgs = dockerArgs.map(arg => {
|
||||
if (typeof arg !== 'string') return arg;
|
||||
|
||||
// Check if this is an environment variable assignment
|
||||
const envMatch = arg.match(/^([A-Z_]+)=(.*)$/);
|
||||
if (envMatch) {
|
||||
const envKey = envMatch[1];
|
||||
const sensitiveSKeys = [
|
||||
'GITHUB_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_SESSION_TOKEN'
|
||||
];
|
||||
if (sensitiveSKeys.includes(envKey)) {
|
||||
return `${envKey}=[REDACTED]`;
|
||||
}
|
||||
// For the command, also redact to avoid logging the full command
|
||||
if (envKey === 'COMMAND') {
|
||||
return `${envKey}=[COMMAND_CONTENT]`;
|
||||
}
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
|
||||
try {
|
||||
logger.info({ dockerArgs: sanitizedArgs }, 'Executing Docker command');
|
||||
|
||||
// Clear any temporary command files after execution
|
||||
const cleanupTempFiles = () => {
|
||||
try {
|
||||
const tempFiles = execFileSync('find', ['/tmp', '-name', 'claude-command-*.txt', '-type', 'f'])
|
||||
.toString()
|
||||
.split('\n');
|
||||
tempFiles
|
||||
.filter(f => f)
|
||||
.forEach(file => {
|
||||
try {
|
||||
fsSync.unlinkSync(file);
|
||||
logger.info(`Removed temp file: ${file}`);
|
||||
} catch {
|
||||
logger.warn(`Failed to remove temp file: ${file}`);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
logger.warn('Failed to clean up temp files');
|
||||
}
|
||||
};
|
||||
|
||||
// Get container lifetime from environment variable or use default (2 hours)
|
||||
const containerLifetimeMs = parseInt(process.env.CONTAINER_LIFETIME_MS, 10) || 7200000; // 2 hours in milliseconds
|
||||
logger.info({ containerLifetimeMs }, 'Setting container lifetime');
|
||||
|
||||
// Use promisified version of child_process.execFile (safer than exec)
|
||||
const { promisify } = require('util');
|
||||
const execFileAsync = promisify(require('child_process').execFile);
|
||||
|
||||
const result = await execFileAsync('docker', dockerArgs, {
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
timeout: containerLifetimeMs // Container lifetime in milliseconds
|
||||
});
|
||||
|
||||
// Clean up temporary files used for command passing
|
||||
cleanupTempFiles();
|
||||
|
||||
let responseText = result.stdout.trim();
|
||||
|
||||
// Check for empty response
|
||||
if (!responseText) {
|
||||
logger.warn(
|
||||
{
|
||||
containerName,
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'Empty response from Claude Code container'
|
||||
);
|
||||
|
||||
// Try to get container logs as the response instead
|
||||
try {
|
||||
responseText = execFileSync('docker', ['logs', containerName], {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
logger.info('Retrieved response from container logs');
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
{
|
||||
error: e.message,
|
||||
containerName
|
||||
},
|
||||
'Failed to get container logs as fallback'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize response to prevent infinite loops by removing bot mentions
|
||||
responseText = sanitizeBotMentions(responseText);
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: repoFullName,
|
||||
issue: issueNumber,
|
||||
responseLength: responseText.length,
|
||||
containerName,
|
||||
stdout: responseText.substring(0, 500) // Log first 500 chars
|
||||
},
|
||||
'Claude Code execution completed successfully'
|
||||
);
|
||||
|
||||
return responseText;
|
||||
} catch (error) {
|
||||
// Clean up temporary files even when there's an error
|
||||
try {
|
||||
const tempFiles = execFileSync('find', ['/tmp', '-name', 'claude-command-*.txt', '-type', 'f'])
|
||||
.toString()
|
||||
.split('\n');
|
||||
tempFiles
|
||||
.filter(f => f)
|
||||
.forEach(file => {
|
||||
try {
|
||||
fsSync.unlinkSync(file);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
// Sanitize stderr and stdout to remove any potential credentials
|
||||
const sanitizeOutput = output => {
|
||||
if (!output) return output;
|
||||
// Import the sanitization utility
|
||||
let sanitized = output.toString();
|
||||
|
||||
// Sensitive values to redact
|
||||
const sensitiveValues = [
|
||||
githubToken,
|
||||
secureCredentials.get('ANTHROPIC_API_KEY'),
|
||||
envVars.AWS_ACCESS_KEY_ID,
|
||||
envVars.AWS_SECRET_ACCESS_KEY,
|
||||
envVars.AWS_SESSION_TOKEN
|
||||
].filter(val => val && val.length > 0);
|
||||
|
||||
// Redact specific sensitive values first
|
||||
sensitiveValues.forEach(value => {
|
||||
if (value) {
|
||||
// Convert to string and escape regex special characters
|
||||
const stringValue = String(value);
|
||||
// Escape regex special characters
|
||||
const escapedValue = stringValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
sanitized = sanitized.replace(new RegExp(escapedValue, 'g'), '[REDACTED]');
|
||||
}
|
||||
});
|
||||
|
||||
// Then apply pattern-based redaction for any missed credentials
|
||||
const sensitivePatterns = [
|
||||
/AKIA[0-9A-Z]{16}/g, // AWS Access Key pattern
|
||||
/[a-zA-Z0-9/+=]{40}/g, // AWS Secret Key pattern
|
||||
/sk-[a-zA-Z0-9]{32,}/g, // API key pattern
|
||||
/github_pat_[a-zA-Z0-9_]{82}/g, // GitHub fine-grained token pattern
|
||||
/ghp_[a-zA-Z0-9]{36}/g // GitHub personal access token pattern
|
||||
];
|
||||
|
||||
sensitivePatterns.forEach(pattern => {
|
||||
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
// Check for specific error types
|
||||
const errorMsg = error.message || '';
|
||||
const errorOutput = error.stderr ? error.stderr.toString() : '';
|
||||
|
||||
// Check if this is a docker image not found error
|
||||
if (
|
||||
errorOutput.includes('Unable to find image') ||
|
||||
errorMsg.includes('Unable to find image')
|
||||
) {
|
||||
logger.error('Docker image not found. Attempting to rebuild...');
|
||||
try {
|
||||
execFileSync('docker', ['build', '-f', 'Dockerfile.claudecode', '-t', dockerImageName, '.'], {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
logger.info('Successfully rebuilt Docker image');
|
||||
} catch (rebuildError) {
|
||||
logger.error(
|
||||
{
|
||||
error: rebuildError.message
|
||||
},
|
||||
'Failed to rebuild Docker image'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(
|
||||
{
|
||||
error: error.message,
|
||||
stderr: sanitizeOutput(error.stderr),
|
||||
stdout: sanitizeOutput(error.stdout),
|
||||
containerName,
|
||||
dockerArgs: sanitizedArgs
|
||||
},
|
||||
'Error running Claude Code container'
|
||||
);
|
||||
|
||||
// Try to get container logs for debugging
|
||||
try {
|
||||
const logs = execFileSync('docker', ['logs', containerName], {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
logger.error({ containerLogs: logs }, 'Container logs');
|
||||
} catch (e) {
|
||||
logger.error({ error: e.message }, 'Failed to get container logs');
|
||||
}
|
||||
|
||||
// Try to clean up the container if it's still running
|
||||
try {
|
||||
execFileSync('docker', ['kill', containerName], { stdio: 'ignore' });
|
||||
} catch {
|
||||
// Container might already be stopped
|
||||
}
|
||||
|
||||
// Generate an error ID for log correlation
|
||||
const timestamp = new Date().toISOString();
|
||||
const errorId = `err-${Math.random().toString(36).substring(2, 10)}`;
|
||||
|
||||
// Log the detailed error with full context
|
||||
const sanitizedStderr = sanitizeOutput(error.stderr);
|
||||
const sanitizedStdout = sanitizeOutput(error.stdout);
|
||||
|
||||
logger.error(
|
||||
{
|
||||
errorId,
|
||||
timestamp,
|
||||
error: error.message,
|
||||
stderr: sanitizedStderr,
|
||||
stdout: sanitizedStdout,
|
||||
containerName,
|
||||
dockerArgs: sanitizedArgs,
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'Claude Code container execution failed (with error reference)'
|
||||
);
|
||||
|
||||
// Throw a generic error with reference ID, but without sensitive details
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`Error executing Claude command (Reference: ${errorId}, Time: ${timestamp})`
|
||||
);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
// Sanitize the error message to remove any credentials
|
||||
const sanitizeMessage = message => {
|
||||
if (!message) return message;
|
||||
let sanitized = message;
|
||||
const sensitivePatterns = [
|
||||
/AWS_ACCESS_KEY_ID="[^"]+"/g,
|
||||
/AWS_SECRET_ACCESS_KEY="[^"]+"/g,
|
||||
/AWS_SESSION_TOKEN="[^"]+"/g,
|
||||
/GITHUB_TOKEN="[^"]+"/g,
|
||||
/ANTHROPIC_API_KEY="[^"]+"/g,
|
||||
/AKIA[0-9A-Z]{16}/g, // AWS Access Key pattern
|
||||
/[a-zA-Z0-9/+=]{40}/g, // AWS Secret Key pattern
|
||||
/sk-[a-zA-Z0-9]{32,}/g, // API key pattern
|
||||
/github_pat_[a-zA-Z0-9_]{82}/g, // GitHub fine-grained token pattern
|
||||
/ghp_[a-zA-Z0-9]{36}/g // GitHub personal access token pattern
|
||||
];
|
||||
|
||||
sensitivePatterns.forEach(pattern => {
|
||||
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
||||
});
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
logger.error(
|
||||
{
|
||||
err: {
|
||||
message: sanitizeMessage(error.message),
|
||||
stack: sanitizeMessage(error.stack)
|
||||
},
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'Error processing command with Claude'
|
||||
);
|
||||
|
||||
// Generate an error ID for log correlation
|
||||
const timestamp = new Date().toISOString();
|
||||
const errorId = `err-${Math.random().toString(36).substring(2, 10)}`;
|
||||
|
||||
// Log the sanitized error with its ID for correlation
|
||||
const sanitizedErrorMessage = sanitizeMessage(error.message);
|
||||
const sanitizedErrorStack = error.stack ? sanitizeMessage(error.stack) : null;
|
||||
|
||||
logger.error(
|
||||
{
|
||||
errorId,
|
||||
timestamp,
|
||||
error: sanitizedErrorMessage,
|
||||
stack: sanitizedErrorStack,
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'General error in Claude service (with error reference)'
|
||||
);
|
||||
|
||||
// Throw a generic error with reference ID, but without sensitive details
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`Error processing Claude command (Reference: ${errorId}, Time: ${timestamp})`
|
||||
);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processCommand
|
||||
};
|
||||
708
src/services/claudeService.ts
Normal file
708
src/services/claudeService.ts
Normal file
@@ -0,0 +1,708 @@
|
||||
import { execFileSync } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { execFile } from 'child_process';
|
||||
import path from 'path';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { sanitizeBotMentions } from '../utils/sanitize';
|
||||
import secureCredentials from '../utils/secureCredentials';
|
||||
import type {
|
||||
ClaudeCommandOptions,
|
||||
OperationType,
|
||||
ClaudeEnvironmentVars,
|
||||
DockerExecutionOptions,
|
||||
ContainerSecurityConfig,
|
||||
ClaudeResourceLimits
|
||||
} from '../types/claude';
|
||||
|
||||
const logger = createLogger('claudeService');
|
||||
|
||||
// Get bot username from environment variables - required
|
||||
const BOT_USERNAME = process.env['BOT_USERNAME'];
|
||||
|
||||
// Validate bot username is set
|
||||
if (!BOT_USERNAME) {
|
||||
logger.error(
|
||||
'BOT_USERNAME environment variable is not set in claudeService. This is required to prevent infinite loops.'
|
||||
);
|
||||
throw new Error('BOT_USERNAME environment variable is required');
|
||||
}
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Processes a command using Claude Code CLI
|
||||
*/
|
||||
export async function processCommand({
|
||||
repoFullName,
|
||||
issueNumber,
|
||||
command,
|
||||
isPullRequest = false,
|
||||
branchName = null,
|
||||
operationType = 'default'
|
||||
}: ClaudeCommandOptions): Promise<string> {
|
||||
try {
|
||||
logger.info(
|
||||
{
|
||||
repo: repoFullName,
|
||||
issue: issueNumber,
|
||||
isPullRequest,
|
||||
branchName,
|
||||
commandLength: command.length
|
||||
},
|
||||
'Processing command with Claude'
|
||||
);
|
||||
|
||||
const githubToken = secureCredentials.get('GITHUB_TOKEN');
|
||||
|
||||
// In test mode, skip execution and return a mock response
|
||||
// Support both classic (ghp_) and fine-grained (github_pat_) GitHub tokens
|
||||
const isValidGitHubToken =
|
||||
githubToken && (githubToken.includes('ghp_') || githubToken.includes('github_pat_'));
|
||||
if (process.env['NODE_ENV'] === 'test' || !isValidGitHubToken) {
|
||||
logger.info(
|
||||
{
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'TEST MODE: Skipping Claude execution'
|
||||
);
|
||||
|
||||
// Create a test response and sanitize it
|
||||
const testResponse = `Hello! I'm Claude responding to your request.
|
||||
|
||||
Since this is a test environment, I'm providing a simulated response. In production, I would:
|
||||
1. Clone the repository ${repoFullName}
|
||||
2. ${isPullRequest ? `Checkout PR branch: ${branchName}` : 'Use the main branch'}
|
||||
3. Analyze the codebase and execute: "${command}"
|
||||
4. Use GitHub CLI to interact with issues, PRs, and comments
|
||||
|
||||
For real functionality, please configure valid GitHub and Claude API tokens.`;
|
||||
|
||||
// Always sanitize responses, even in test mode
|
||||
return sanitizeBotMentions(testResponse);
|
||||
}
|
||||
|
||||
// Build Docker image if it doesn't exist
|
||||
const dockerImageName = process.env['CLAUDE_CONTAINER_IMAGE'] ?? 'claudecode:latest';
|
||||
try {
|
||||
execFileSync('docker', ['inspect', dockerImageName], { stdio: 'ignore' });
|
||||
logger.info({ dockerImageName }, 'Docker image already exists');
|
||||
} catch {
|
||||
logger.info({ dockerImageName }, 'Building Docker image for Claude Code runner');
|
||||
execFileSync('docker', ['build', '-f', 'Dockerfile.claudecode', '-t', dockerImageName, '.'], {
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
}
|
||||
|
||||
// Use unified entrypoint script for all operation types
|
||||
const entrypointScript = getEntrypointScript();
|
||||
logger.info(
|
||||
{ operationType },
|
||||
`Using ${operationType === 'auto-tagging' ? 'minimal tools for auto-tagging operation' : 'full tool set for standard operation'}`
|
||||
);
|
||||
|
||||
// Create unique container name (sanitized to prevent command injection)
|
||||
const sanitizedRepoName = repoFullName.replace(/[^a-zA-Z0-9\-_]/g, '-');
|
||||
const containerName = `claude-${sanitizedRepoName}-${Date.now()}`;
|
||||
|
||||
// Create the full prompt with context and instructions based on operation type
|
||||
const fullPrompt = createPrompt({
|
||||
operationType,
|
||||
repoFullName,
|
||||
issueNumber,
|
||||
branchName,
|
||||
isPullRequest,
|
||||
command
|
||||
});
|
||||
|
||||
// Prepare environment variables for the container
|
||||
const envVars = createEnvironmentVars({
|
||||
repoFullName,
|
||||
issueNumber,
|
||||
isPullRequest,
|
||||
branchName,
|
||||
operationType,
|
||||
fullPrompt,
|
||||
githubToken
|
||||
});
|
||||
|
||||
// Run the container
|
||||
logger.info(
|
||||
{
|
||||
containerName,
|
||||
repo: repoFullName,
|
||||
isPullRequest,
|
||||
branch: branchName
|
||||
},
|
||||
'Starting Claude Code container'
|
||||
);
|
||||
|
||||
// Build docker run command as an array to prevent command injection
|
||||
const dockerArgs = buildDockerArgs({
|
||||
containerName,
|
||||
entrypointScript,
|
||||
dockerImageName,
|
||||
envVars
|
||||
});
|
||||
|
||||
// Create sanitized version for logging (remove sensitive values)
|
||||
const sanitizedArgs = sanitizeDockerArgs(dockerArgs);
|
||||
|
||||
try {
|
||||
logger.info({ dockerArgs: sanitizedArgs }, 'Executing Docker command');
|
||||
|
||||
// Get container lifetime from environment variable or use default (2 hours)
|
||||
const containerLifetimeMs = parseInt(process.env['CONTAINER_LIFETIME_MS'] ?? '7200000', 10);
|
||||
logger.info({ containerLifetimeMs }, 'Setting container lifetime');
|
||||
|
||||
const executionOptions: DockerExecutionOptions = {
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
timeout: containerLifetimeMs // Container lifetime in milliseconds
|
||||
};
|
||||
|
||||
const result = await execFileAsync('docker', dockerArgs, executionOptions);
|
||||
|
||||
let responseText = result.stdout.trim();
|
||||
|
||||
// Check for empty response
|
||||
if (!responseText) {
|
||||
logger.warn(
|
||||
{
|
||||
containerName,
|
||||
repo: repoFullName,
|
||||
issue: issueNumber
|
||||
},
|
||||
'Empty response from Claude Code container'
|
||||
);
|
||||
|
||||
// Try to get container logs as the response instead
|
||||
try {
|
||||
responseText = execFileSync('docker', ['logs', containerName], {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
logger.info('Retrieved response from container logs');
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
{
|
||||
error: (e as Error).message,
|
||||
containerName
|
||||
},
|
||||
'Failed to get container logs as fallback'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize response to prevent infinite loops by removing bot mentions
|
||||
responseText = sanitizeBotMentions(responseText);
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: repoFullName,
|
||||
issue: issueNumber,
|
||||
responseLength: responseText.length,
|
||||
containerName,
|
||||
stdout: responseText.substring(0, 500) // Log first 500 chars
|
||||
},
|
||||
'Claude Code execution completed successfully'
|
||||
);
|
||||
|
||||
return responseText;
|
||||
} catch (error) {
|
||||
return handleDockerExecutionError(error, {
|
||||
containerName,
|
||||
dockerArgs: sanitizedArgs,
|
||||
dockerImageName,
|
||||
githubToken,
|
||||
repoFullName,
|
||||
issueNumber
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return handleGeneralError(error, { repoFullName, issueNumber });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entrypoint script for Claude Code execution
|
||||
* Uses unified entrypoint that handles all operation types based on OPERATION_TYPE env var
|
||||
*/
|
||||
function getEntrypointScript(): string {
|
||||
return '/scripts/runtime/claudecode-entrypoint.sh';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create prompt based on operation type and context
|
||||
*/
|
||||
function createPrompt({
|
||||
operationType,
|
||||
repoFullName,
|
||||
issueNumber,
|
||||
branchName,
|
||||
isPullRequest,
|
||||
command
|
||||
}: {
|
||||
operationType: OperationType;
|
||||
repoFullName: string;
|
||||
issueNumber: number | null;
|
||||
branchName: string | null;
|
||||
isPullRequest: boolean;
|
||||
command: string;
|
||||
}): string {
|
||||
if (operationType === 'auto-tagging') {
|
||||
return `You are Claude, an AI assistant analyzing a GitHub issue for automatic label assignment.
|
||||
|
||||
**Context:**
|
||||
- Repository: ${repoFullName}
|
||||
- Issue Number: #${issueNumber}
|
||||
- Operation: Auto-tagging (Read-only + Label assignment)
|
||||
|
||||
**Available Tools:**
|
||||
- Read: Access repository files and issue content
|
||||
- GitHub: Use 'gh' CLI for label operations only
|
||||
|
||||
**Task:**
|
||||
Analyze the issue and apply appropriate labels using GitHub CLI commands. Use these categories:
|
||||
- Priority: critical, high, medium, low
|
||||
- Type: bug, feature, enhancement, documentation, question, security
|
||||
- Complexity: trivial, simple, moderate, complex
|
||||
- Component: api, frontend, backend, database, auth, webhook, docker
|
||||
|
||||
**Process:**
|
||||
1. First run 'gh label list' to see available labels
|
||||
2. Analyze the issue content
|
||||
3. Use 'gh issue edit #{issueNumber} --add-label "label1,label2,label3"' to apply labels
|
||||
4. Do NOT comment on the issue - only apply labels
|
||||
|
||||
**User Request:**
|
||||
${command}
|
||||
|
||||
Complete the auto-tagging task using only the minimal required tools.`;
|
||||
} else {
|
||||
return `You are ${process.env.BOT_USERNAME}, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'}.
|
||||
|
||||
**Context:**
|
||||
- Repository: ${repoFullName}
|
||||
- ${isPullRequest ? 'Pull Request' : 'Issue'} Number: #${issueNumber}
|
||||
- Current Branch: ${branchName ?? 'main'}
|
||||
- Running in: Unattended mode
|
||||
|
||||
**Important Instructions:**
|
||||
1. You have full GitHub CLI access via the 'gh' command
|
||||
2. When writing code:
|
||||
- Always create a feature branch for new work
|
||||
- Make commits with descriptive messages
|
||||
- Push your work to the remote repository
|
||||
- Run all tests and ensure they pass
|
||||
- Fix any linting or type errors
|
||||
- Create a pull request if appropriate
|
||||
3. Iterate until the task is complete - don't stop at partial solutions
|
||||
4. Always check in your work by pushing to the remote before finishing
|
||||
5. Use 'gh issue comment' or 'gh pr comment' to provide updates on your progress
|
||||
6. If you encounter errors, debug and fix them before completing
|
||||
7. **IMPORTANT - Markdown Formatting:**
|
||||
- When your response contains markdown (like headers, lists, code blocks), return it as properly formatted markdown
|
||||
- Do NOT escape or encode special characters like newlines (\\n) or quotes
|
||||
- Return clean, human-readable markdown that GitHub will render correctly
|
||||
- Your response should look like normal markdown text, not escaped strings
|
||||
8. **Request Acknowledgment:**
|
||||
- For larger or complex tasks that will take significant time, first acknowledge the request
|
||||
- Post a brief comment like "I understand. Working on [task description]..." before starting
|
||||
- Use 'gh issue comment' or 'gh pr comment' to post this acknowledgment immediately
|
||||
- This lets the user know their request was received and is being processed
|
||||
|
||||
**User Request:**
|
||||
${command}
|
||||
|
||||
Please complete this task fully and autonomously.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create environment variables for container
|
||||
*/
|
||||
function createEnvironmentVars({
|
||||
repoFullName,
|
||||
issueNumber,
|
||||
isPullRequest,
|
||||
branchName,
|
||||
operationType,
|
||||
fullPrompt,
|
||||
githubToken
|
||||
}: {
|
||||
repoFullName: string;
|
||||
issueNumber: number | null;
|
||||
isPullRequest: boolean;
|
||||
branchName: string | null;
|
||||
operationType: OperationType;
|
||||
fullPrompt: string;
|
||||
githubToken: string;
|
||||
}): ClaudeEnvironmentVars {
|
||||
return {
|
||||
REPO_FULL_NAME: repoFullName,
|
||||
ISSUE_NUMBER: issueNumber?.toString() ?? '',
|
||||
IS_PULL_REQUEST: isPullRequest ? 'true' : 'false',
|
||||
BRANCH_NAME: branchName ?? '',
|
||||
OPERATION_TYPE: operationType,
|
||||
COMMAND: fullPrompt,
|
||||
GITHUB_TOKEN: githubToken,
|
||||
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY') ?? '',
|
||||
BOT_USERNAME: process.env.BOT_USERNAME,
|
||||
BOT_EMAIL: process.env.BOT_EMAIL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Docker arguments array
|
||||
*/
|
||||
function buildDockerArgs({
|
||||
containerName,
|
||||
entrypointScript,
|
||||
dockerImageName,
|
||||
envVars
|
||||
}: {
|
||||
containerName: string;
|
||||
entrypointScript: string;
|
||||
dockerImageName: string;
|
||||
envVars: ClaudeEnvironmentVars;
|
||||
}): string[] {
|
||||
const dockerArgs = ['run', '--rm'];
|
||||
|
||||
// Apply container security constraints
|
||||
const securityConfig = getContainerSecurityConfig();
|
||||
applySecurityConstraints(dockerArgs, securityConfig);
|
||||
|
||||
// Add container name
|
||||
dockerArgs.push('--name', containerName);
|
||||
|
||||
// Add Claude authentication directory as a volume mount for syncing
|
||||
// This allows the entrypoint to copy auth files to a writable location
|
||||
const hostAuthDir = process.env.CLAUDE_AUTH_HOST_DIR;
|
||||
if (hostAuthDir) {
|
||||
// Resolve relative paths to absolute paths for Docker volume mounting
|
||||
const path = require('path');
|
||||
const absoluteAuthDir = path.isAbsolute(hostAuthDir)
|
||||
? hostAuthDir
|
||||
: path.resolve(process.cwd(), hostAuthDir);
|
||||
dockerArgs.push('-v', `${absoluteAuthDir}:/home/node/.claude`);
|
||||
}
|
||||
|
||||
// Add environment variables as separate arguments
|
||||
Object.entries(envVars)
|
||||
.filter(([, value]) => value !== undefined && value !== '')
|
||||
.forEach(([key, value]) => {
|
||||
dockerArgs.push('-e', `${key}=${String(value)}`);
|
||||
});
|
||||
|
||||
// Add the image name and custom entrypoint
|
||||
dockerArgs.push('--entrypoint', entrypointScript, dockerImageName);
|
||||
|
||||
return dockerArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container security configuration
|
||||
*/
|
||||
function getContainerSecurityConfig(): ContainerSecurityConfig {
|
||||
const resourceLimits: ClaudeResourceLimits = {
|
||||
memory: process.env.CLAUDE_CONTAINER_MEMORY_LIMIT ?? '2g',
|
||||
cpuShares: process.env.CLAUDE_CONTAINER_CPU_SHARES ?? '1024',
|
||||
pidsLimit: process.env.CLAUDE_CONTAINER_PIDS_LIMIT ?? '256'
|
||||
};
|
||||
|
||||
if (process.env.CLAUDE_CONTAINER_PRIVILEGED === 'true') {
|
||||
return {
|
||||
privileged: true,
|
||||
requiredCapabilities: [],
|
||||
optionalCapabilities: {},
|
||||
resourceLimits
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
privileged: false,
|
||||
requiredCapabilities: ['NET_ADMIN', 'SYS_ADMIN'],
|
||||
optionalCapabilities: {
|
||||
NET_RAW: process.env.CLAUDE_CONTAINER_CAP_NET_RAW === 'true',
|
||||
SYS_TIME: process.env.CLAUDE_CONTAINER_CAP_SYS_TIME === 'true',
|
||||
DAC_OVERRIDE: process.env.CLAUDE_CONTAINER_CAP_DAC_OVERRIDE === 'true',
|
||||
AUDIT_WRITE: process.env.CLAUDE_CONTAINER_CAP_AUDIT_WRITE === 'true'
|
||||
},
|
||||
resourceLimits
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply security constraints to Docker arguments
|
||||
*/
|
||||
function applySecurityConstraints(dockerArgs: string[], config: ContainerSecurityConfig): void {
|
||||
if (config.privileged) {
|
||||
dockerArgs.push('--privileged');
|
||||
} else {
|
||||
// Add required capabilities
|
||||
config.requiredCapabilities.forEach(cap => {
|
||||
dockerArgs.push(`--cap-add=${cap}`);
|
||||
});
|
||||
|
||||
// Add optional capabilities if enabled
|
||||
Object.entries(config.optionalCapabilities).forEach(([cap, enabled]) => {
|
||||
if (enabled) {
|
||||
dockerArgs.push(`--cap-add=${cap}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Add resource limits
|
||||
dockerArgs.push(
|
||||
'--memory',
|
||||
config.resourceLimits.memory,
|
||||
'--cpu-shares',
|
||||
config.resourceLimits.cpuShares,
|
||||
'--pids-limit',
|
||||
config.resourceLimits.pidsLimit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize Docker arguments for logging
|
||||
*/
|
||||
function sanitizeDockerArgs(dockerArgs: string[]): string[] {
|
||||
return dockerArgs.map(arg => {
|
||||
if (typeof arg !== 'string') return arg;
|
||||
|
||||
// Check if this is an environment variable assignment
|
||||
const envMatch = arg.match(/^([A-Z_]+)=(.*)$/);
|
||||
if (envMatch) {
|
||||
const envKey = envMatch[1];
|
||||
const sensitiveKeys = [
|
||||
'GITHUB_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_SESSION_TOKEN'
|
||||
];
|
||||
if (sensitiveKeys.includes(envKey)) {
|
||||
return `${envKey}=[REDACTED]`;
|
||||
}
|
||||
// For the command, also redact to avoid logging the full command
|
||||
if (envKey === 'COMMAND') {
|
||||
return `${envKey}=[COMMAND_CONTENT]`;
|
||||
}
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Docker execution errors
|
||||
*/
|
||||
function handleDockerExecutionError(
|
||||
error: unknown,
|
||||
context: {
|
||||
containerName: string;
|
||||
dockerArgs: string[];
|
||||
dockerImageName: string;
|
||||
githubToken: string;
|
||||
repoFullName: string;
|
||||
issueNumber: number | null;
|
||||
}
|
||||
): never {
|
||||
const err = error as Error & { stderr?: string; stdout?: string; message: string };
|
||||
|
||||
// Sanitize stderr and stdout to remove any potential credentials
|
||||
const sanitizeOutput = (output: string | undefined): string | undefined => {
|
||||
if (!output) return output;
|
||||
let sanitized = output.toString();
|
||||
|
||||
// Sensitive values to redact
|
||||
const sensitiveValues = [
|
||||
context.githubToken,
|
||||
secureCredentials.get('ANTHROPIC_API_KEY')
|
||||
].filter(val => val && val.length > 0);
|
||||
|
||||
// Redact specific sensitive values first
|
||||
sensitiveValues.forEach(value => {
|
||||
if (value) {
|
||||
const stringValue = String(value);
|
||||
const escapedValue = stringValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
sanitized = sanitized.replace(new RegExp(escapedValue, 'g'), '[REDACTED]');
|
||||
}
|
||||
});
|
||||
|
||||
// Then apply pattern-based redaction for any missed credentials
|
||||
const sensitivePatterns = [
|
||||
/AKIA[0-9A-Z]{16}/g, // AWS Access Key pattern
|
||||
/[a-zA-Z0-9/+=]{40}/g, // AWS Secret Key pattern
|
||||
/sk-[a-zA-Z0-9]{32,}/g, // API key pattern
|
||||
/github_pat_[a-zA-Z0-9_]{82}/g, // GitHub fine-grained token pattern
|
||||
/ghp_[a-zA-Z0-9]{36}/g // GitHub personal access token pattern
|
||||
];
|
||||
|
||||
sensitivePatterns.forEach(pattern => {
|
||||
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
// Check for specific error types
|
||||
const errorMsg = err.message;
|
||||
const errorOutput = err.stderr ? err.stderr.toString() : '';
|
||||
|
||||
// Check if this is a docker image not found error
|
||||
if (errorOutput.includes('Unable to find image') || errorMsg.includes('Unable to find image')) {
|
||||
logger.error('Docker image not found. Attempting to rebuild...');
|
||||
try {
|
||||
execFileSync(
|
||||
'docker',
|
||||
['build', '-f', 'Dockerfile.claudecode', '-t', context.dockerImageName, '.'],
|
||||
{
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
stdio: 'pipe'
|
||||
}
|
||||
);
|
||||
logger.info('Successfully rebuilt Docker image');
|
||||
} catch (rebuildError) {
|
||||
logger.error(
|
||||
{
|
||||
error: (rebuildError as Error).message
|
||||
},
|
||||
'Failed to rebuild Docker image'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(
|
||||
{
|
||||
error: err.message,
|
||||
stderr: sanitizeOutput(err.stderr),
|
||||
stdout: sanitizeOutput(err.stdout),
|
||||
containerName: context.containerName,
|
||||
dockerArgs: context.dockerArgs
|
||||
},
|
||||
'Error running Claude Code container'
|
||||
);
|
||||
|
||||
// Try to get container logs for debugging
|
||||
try {
|
||||
const logs = execFileSync('docker', ['logs', context.containerName], {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
logger.error({ containerLogs: logs }, 'Container logs');
|
||||
} catch (e) {
|
||||
logger.error({ error: (e as Error).message }, 'Failed to get container logs');
|
||||
}
|
||||
|
||||
// Try to clean up the container if it's still running
|
||||
try {
|
||||
execFileSync('docker', ['kill', context.containerName], { stdio: 'ignore' });
|
||||
} catch {
|
||||
// Container might already be stopped
|
||||
}
|
||||
|
||||
// Generate an error ID for log correlation
|
||||
const timestamp = new Date().toISOString();
|
||||
const errorId = `err-${Math.random().toString(36).substring(2, 10)}`;
|
||||
|
||||
// Log the detailed error with full context
|
||||
const sanitizedStderr = sanitizeOutput(err.stderr);
|
||||
const sanitizedStdout = sanitizeOutput(err.stdout);
|
||||
|
||||
logger.error(
|
||||
{
|
||||
errorId,
|
||||
timestamp,
|
||||
error: err.message,
|
||||
stderr: sanitizedStderr,
|
||||
stdout: sanitizedStdout,
|
||||
containerName: context.containerName,
|
||||
dockerArgs: context.dockerArgs,
|
||||
repo: context.repoFullName,
|
||||
issue: context.issueNumber
|
||||
},
|
||||
'Claude Code container execution failed (with error reference)'
|
||||
);
|
||||
|
||||
// Throw a generic error with reference ID, but without sensitive details
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`Error executing Claude command (Reference: ${errorId}, Time: ${timestamp})`
|
||||
);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general service errors
|
||||
*/
|
||||
function handleGeneralError(
|
||||
error: unknown,
|
||||
context: { repoFullName: string; issueNumber: number | null }
|
||||
): never {
|
||||
const err = error as Error;
|
||||
|
||||
// Sanitize the error message to remove any credentials
|
||||
const sanitizeMessage = (message: string): string => {
|
||||
if (!message) return message;
|
||||
let sanitized = message;
|
||||
const sensitivePatterns = [
|
||||
/AWS_ACCESS_KEY_ID="[^"]+"/g,
|
||||
/AWS_SECRET_ACCESS_KEY="[^"]+"/g,
|
||||
/AWS_SESSION_TOKEN="[^"]+"/g,
|
||||
/GITHUB_TOKEN="[^"]+"/g,
|
||||
/ANTHROPIC_API_KEY="[^"]+"/g,
|
||||
/AKIA[0-9A-Z]{16}/g, // AWS Access Key pattern
|
||||
/[a-zA-Z0-9/+=]{40}/g, // AWS Secret Key pattern
|
||||
/sk-[a-zA-Z0-9]{32,}/g, // API key pattern
|
||||
/github_pat_[a-zA-Z0-9_]{82}/g, // GitHub fine-grained token pattern
|
||||
/ghp_[a-zA-Z0-9]{36}/g // GitHub personal access token pattern
|
||||
];
|
||||
|
||||
sensitivePatterns.forEach(pattern => {
|
||||
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
||||
});
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
logger.error(
|
||||
{
|
||||
err: {
|
||||
message: sanitizeMessage(err.message),
|
||||
stack: sanitizeMessage(err.stack ?? '')
|
||||
},
|
||||
repo: context.repoFullName,
|
||||
issue: context.issueNumber
|
||||
},
|
||||
'Error processing command with Claude'
|
||||
);
|
||||
|
||||
// Generate an error ID for log correlation
|
||||
const timestamp = new Date().toISOString();
|
||||
const errorId = `err-${Math.random().toString(36).substring(2, 10)}`;
|
||||
|
||||
// Log the sanitized error with its ID for correlation
|
||||
const sanitizedErrorMessage = sanitizeMessage(err.message);
|
||||
const sanitizedErrorStack = err.stack ? sanitizeMessage(err.stack) : null;
|
||||
|
||||
logger.error(
|
||||
{
|
||||
errorId,
|
||||
timestamp,
|
||||
error: sanitizedErrorMessage,
|
||||
stack: sanitizedErrorStack,
|
||||
repo: context.repoFullName,
|
||||
issue: context.issueNumber
|
||||
},
|
||||
'General error in Claude service (with error reference)'
|
||||
);
|
||||
|
||||
// Throw a generic error with reference ID, but without sensitive details
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`Error processing Claude command (Reference: ${errorId}, Time: ${timestamp})`
|
||||
);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const { createLogger } = require('../utils/logger');
|
||||
const secureCredentials = require('../utils/secureCredentials');
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import secureCredentials from '../utils/secureCredentials';
|
||||
import type {
|
||||
CreateCommentRequest,
|
||||
CreateCommentResponse,
|
||||
AddLabelsRequest,
|
||||
ManagePRLabelsRequest,
|
||||
CreateRepositoryLabelsRequest,
|
||||
GetCombinedStatusRequest,
|
||||
HasReviewedPRRequest,
|
||||
GetCheckSuitesRequest,
|
||||
ValidatedGitHubParams,
|
||||
GitHubCombinedStatus,
|
||||
GitHubLabel,
|
||||
GitHubCheckSuitesResponse
|
||||
} from '../types/github';
|
||||
|
||||
const logger = createLogger('githubService');
|
||||
|
||||
// Create Octokit instance (lazy initialization)
|
||||
let octokit = null;
|
||||
let octokit: Octokit | null = null;
|
||||
|
||||
function getOctokit() {
|
||||
function getOctokit(): Octokit | null {
|
||||
if (!octokit) {
|
||||
const githubToken = secureCredentials.get('GITHUB_TOKEN');
|
||||
if (githubToken && githubToken.includes('ghp_')) {
|
||||
// Support both classic (ghp_) and fine-grained (github_pat_) GitHub tokens
|
||||
if (githubToken && (githubToken.includes('ghp_') || githubToken.includes('github_pat_'))) {
|
||||
octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
userAgent: 'Claude-GitHub-Webhook'
|
||||
@@ -23,7 +38,12 @@ function getOctokit() {
|
||||
/**
|
||||
* Posts a comment to a GitHub issue or pull request
|
||||
*/
|
||||
async function postComment({ repoOwner, repoName, issueNumber, body }) {
|
||||
export async function postComment({
|
||||
repoOwner,
|
||||
repoName,
|
||||
issueNumber,
|
||||
body
|
||||
}: CreateCommentRequest): Promise<CreateCommentResponse> {
|
||||
try {
|
||||
// Validate parameters to prevent SSRF
|
||||
const validated = validateGitHubParams(repoOwner, repoName, issueNumber);
|
||||
@@ -72,13 +92,18 @@ async function postComment({ repoOwner, repoName, issueNumber, body }) {
|
||||
'Comment posted successfully'
|
||||
);
|
||||
|
||||
return data;
|
||||
return {
|
||||
id: data.id,
|
||||
body: data.body ?? '',
|
||||
created_at: data.created_at
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error as Error & { response?: { data?: unknown } };
|
||||
logger.error(
|
||||
{
|
||||
err: {
|
||||
message: error.message,
|
||||
responseData: error.response?.data
|
||||
message: err.message,
|
||||
responseData: err.response?.data
|
||||
},
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
issue: issueNumber
|
||||
@@ -86,14 +111,18 @@ async function postComment({ repoOwner, repoName, issueNumber, body }) {
|
||||
'Error posting comment to GitHub'
|
||||
);
|
||||
|
||||
throw new Error(`Failed to post comment: ${error.message}`);
|
||||
throw new Error(`Failed to post comment: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates GitHub repository and issue parameters to prevent SSRF
|
||||
*/
|
||||
function validateGitHubParams(repoOwner, repoName, issueNumber) {
|
||||
function validateGitHubParams(
|
||||
repoOwner: string,
|
||||
repoName: string,
|
||||
issueNumber: number
|
||||
): ValidatedGitHubParams {
|
||||
// Validate repoOwner and repoName contain only safe characters
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
if (!repoPattern.test(repoOwner) || !repoPattern.test(repoName)) {
|
||||
@@ -101,7 +130,7 @@ function validateGitHubParams(repoOwner, repoName, issueNumber) {
|
||||
}
|
||||
|
||||
// Validate issueNumber is a positive integer
|
||||
const issueNum = parseInt(issueNumber, 10);
|
||||
const issueNum = parseInt(String(issueNumber), 10);
|
||||
if (!Number.isInteger(issueNum) || issueNum <= 0) {
|
||||
throw new Error('Invalid issue number - must be a positive integer');
|
||||
}
|
||||
@@ -112,7 +141,12 @@ function validateGitHubParams(repoOwner, repoName, issueNumber) {
|
||||
/**
|
||||
* Adds labels to a GitHub issue
|
||||
*/
|
||||
async function addLabelsToIssue({ repoOwner, repoName, issueNumber, labels }) {
|
||||
export async function addLabelsToIssue({
|
||||
repoOwner,
|
||||
repoName,
|
||||
issueNumber,
|
||||
labels
|
||||
}: AddLabelsRequest): Promise<GitHubLabel[]> {
|
||||
try {
|
||||
// Validate parameters to prevent SSRF
|
||||
const validated = validateGitHubParams(repoOwner, repoName, issueNumber);
|
||||
@@ -137,10 +171,12 @@ async function addLabelsToIssue({ repoOwner, repoName, issueNumber, labels }) {
|
||||
'TEST MODE: Would add labels to GitHub issue'
|
||||
);
|
||||
|
||||
return {
|
||||
added_labels: labels,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
return labels.map((label, index) => ({
|
||||
id: index,
|
||||
name: label,
|
||||
color: '000000',
|
||||
description: null
|
||||
}));
|
||||
}
|
||||
|
||||
// Use Octokit to add labels
|
||||
@@ -162,11 +198,12 @@ async function addLabelsToIssue({ repoOwner, repoName, issueNumber, labels }) {
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const err = error as Error & { response?: { data?: unknown } };
|
||||
logger.error(
|
||||
{
|
||||
err: {
|
||||
message: error.message,
|
||||
responseData: error.response?.data
|
||||
message: err.message,
|
||||
responseData: err.response?.data
|
||||
},
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
issue: issueNumber,
|
||||
@@ -175,20 +212,25 @@ async function addLabelsToIssue({ repoOwner, repoName, issueNumber, labels }) {
|
||||
'Error adding labels to GitHub issue'
|
||||
);
|
||||
|
||||
throw new Error(`Failed to add labels: ${error.message}`);
|
||||
throw new Error(`Failed to add labels: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates repository labels if they don't exist
|
||||
*/
|
||||
async function createRepositoryLabels({ repoOwner, repoName, labels }) {
|
||||
export async function createRepositoryLabels({
|
||||
repoOwner,
|
||||
repoName,
|
||||
labels
|
||||
}: CreateRepositoryLabelsRequest): Promise<GitHubLabel[]> {
|
||||
try {
|
||||
// Validate repository parameters to prevent SSRF
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
if (!repoPattern.test(repoOwner) || !repoPattern.test(repoName)) {
|
||||
throw new Error('Invalid repository owner or name - contains unsafe characters');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
@@ -207,10 +249,15 @@ async function createRepositoryLabels({ repoOwner, repoName, labels }) {
|
||||
},
|
||||
'TEST MODE: Would create repository labels'
|
||||
);
|
||||
return labels;
|
||||
return labels.map((label, index) => ({
|
||||
id: index,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description ?? null
|
||||
}));
|
||||
}
|
||||
|
||||
const createdLabels = [];
|
||||
const createdLabels: GitHubLabel[] = [];
|
||||
|
||||
for (const label of labels) {
|
||||
try {
|
||||
@@ -226,13 +273,14 @@ async function createRepositoryLabels({ repoOwner, repoName, labels }) {
|
||||
createdLabels.push(data);
|
||||
logger.debug({ labelName: label.name }, 'Label created successfully');
|
||||
} catch (error) {
|
||||
const err = error as Error & { status?: number };
|
||||
// Label might already exist - check if it's a 422 (Unprocessable Entity)
|
||||
if (error.status === 422) {
|
||||
if (err.status === 422) {
|
||||
logger.debug({ labelName: label.name }, 'Label already exists, skipping');
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
err: error.message,
|
||||
err: err.message,
|
||||
labelName: label.name
|
||||
},
|
||||
'Failed to create label'
|
||||
@@ -243,24 +291,25 @@ async function createRepositoryLabels({ repoOwner, repoName, labels }) {
|
||||
|
||||
return createdLabels;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: error.message,
|
||||
err: err.message,
|
||||
repo: `${repoOwner}/${repoName}`
|
||||
},
|
||||
'Error creating repository labels'
|
||||
);
|
||||
|
||||
throw new Error(`Failed to create labels: ${error.message}`);
|
||||
throw new Error(`Failed to create labels: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides fallback labels based on simple keyword matching
|
||||
*/
|
||||
async function getFallbackLabels(title, body) {
|
||||
const content = `${title} ${body || ''}`.toLowerCase();
|
||||
const labels = [];
|
||||
export function getFallbackLabels(title: string, body: string | null): string[] {
|
||||
const content = `${title} ${body ?? ''}`.toLowerCase();
|
||||
const labels: string[] = [];
|
||||
|
||||
// Type detection - check documentation first for specificity
|
||||
if (
|
||||
@@ -335,7 +384,11 @@ async function getFallbackLabels(title, body) {
|
||||
* Gets the combined status for a specific commit/ref
|
||||
* Used to verify all required status checks have passed
|
||||
*/
|
||||
async function getCombinedStatus({ repoOwner, repoName, ref }) {
|
||||
export async function getCombinedStatus({
|
||||
repoOwner,
|
||||
repoName,
|
||||
ref
|
||||
}: GetCombinedStatusRequest): Promise<GitHubCombinedStatus> {
|
||||
try {
|
||||
// Validate parameters to prevent SSRF
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
@@ -372,8 +425,8 @@ async function getCombinedStatus({ repoOwner, repoName, ref }) {
|
||||
state: 'success',
|
||||
total_count: 2,
|
||||
statuses: [
|
||||
{ state: 'success', context: 'ci/test' },
|
||||
{ state: 'success', context: 'ci/build' }
|
||||
{ state: 'success', context: 'ci/test', description: null, target_url: null },
|
||||
{ state: 'success', context: 'ci/build', description: null, target_url: null }
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -397,12 +450,13 @@ async function getCombinedStatus({ repoOwner, repoName, ref }) {
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const err = error as Error & { response?: { status?: number; data?: unknown } };
|
||||
logger.error(
|
||||
{
|
||||
err: {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
message: err.message,
|
||||
status: err.response?.status,
|
||||
responseData: err.response?.data
|
||||
},
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
ref: ref
|
||||
@@ -410,20 +464,19 @@ async function getCombinedStatus({ repoOwner, repoName, ref }) {
|
||||
'Error getting combined status from GitHub'
|
||||
);
|
||||
|
||||
throw new Error(`Failed to get combined status: ${error.message}`);
|
||||
throw new Error(`Failed to get combined status: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we've already reviewed this PR at the given commit SHA
|
||||
* @param {Object} params
|
||||
* @param {string} params.repoOwner - Repository owner
|
||||
* @param {string} params.repoName - Repository name
|
||||
* @param {number} params.prNumber - Pull request number
|
||||
* @param {string} params.commitSha - Commit SHA to check
|
||||
* @returns {Promise<boolean>} True if already reviewed at this SHA
|
||||
*/
|
||||
async function hasReviewedPRAtCommit({ repoOwner, repoName, prNumber, commitSha }) {
|
||||
export async function hasReviewedPRAtCommit({
|
||||
repoOwner,
|
||||
repoName,
|
||||
prNumber,
|
||||
commitSha
|
||||
}: HasReviewedPRRequest): Promise<boolean> {
|
||||
try {
|
||||
// Validate parameters
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
@@ -454,18 +507,18 @@ async function hasReviewedPRAtCommit({ repoOwner, repoName, prNumber, commitSha
|
||||
});
|
||||
|
||||
// Check if any review mentions this specific commit SHA
|
||||
const botUsername = process.env.BOT_USERNAME || 'ClaudeBot';
|
||||
const botUsername = process.env.BOT_USERNAME ?? 'ClaudeBot';
|
||||
const existingReview = reviews.find(review => {
|
||||
return review.user.login === botUsername &&
|
||||
review.body &&
|
||||
review.body.includes(`commit: ${commitSha}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return review.user?.login === botUsername && review.body?.includes(`commit: ${commitSha}`);
|
||||
});
|
||||
|
||||
return !!existingReview;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: error.message,
|
||||
err: err.message,
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
pr: prNumber
|
||||
},
|
||||
@@ -477,15 +530,112 @@ async function hasReviewedPRAtCommit({ repoOwner, repoName, prNumber, commitSha
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove labels on a pull request
|
||||
* @param {Object} params
|
||||
* @param {string} params.repoOwner - Repository owner
|
||||
* @param {string} params.repoName - Repository name
|
||||
* @param {number} params.prNumber - Pull request number
|
||||
* @param {string[]} params.labelsToAdd - Labels to add
|
||||
* @param {string[]} params.labelsToRemove - Labels to remove
|
||||
* Gets check suites for a specific commit
|
||||
*/
|
||||
async function managePRLabels({ repoOwner, repoName, prNumber, labelsToAdd = [], labelsToRemove = [] }) {
|
||||
export async function getCheckSuitesForRef({
|
||||
repoOwner,
|
||||
repoName,
|
||||
ref
|
||||
}: GetCheckSuitesRequest): Promise<GitHubCheckSuitesResponse> {
|
||||
try {
|
||||
// Validate parameters to prevent SSRF
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
if (!repoPattern.test(repoOwner) || !repoPattern.test(repoName)) {
|
||||
throw new Error('Invalid repository owner or name - contains unsafe characters');
|
||||
}
|
||||
|
||||
// Validate ref (commit SHA, branch, or tag)
|
||||
const refPattern = /^[a-zA-Z0-9._/-]+$/;
|
||||
if (!refPattern.test(ref)) {
|
||||
throw new Error('Invalid ref - contains unsafe characters');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
ref
|
||||
},
|
||||
'Getting check suites for ref'
|
||||
);
|
||||
|
||||
// In test mode, return mock data
|
||||
const client = getOctokit();
|
||||
if (process.env.NODE_ENV === 'test' || !client) {
|
||||
return {
|
||||
total_count: 1,
|
||||
check_suites: [
|
||||
{
|
||||
id: 12345,
|
||||
head_branch: 'main',
|
||||
head_sha: ref,
|
||||
status: 'completed',
|
||||
conclusion: 'success',
|
||||
app: { id: 1, slug: 'github-actions', name: 'GitHub Actions' },
|
||||
pull_requests: [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
latest_check_runs_count: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Use Octokit's built-in method
|
||||
const { data } = await client.checks.listSuitesForRef({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
ref: ref
|
||||
});
|
||||
|
||||
// Transform the response to match our interface
|
||||
const transformedResponse: GitHubCheckSuitesResponse = {
|
||||
total_count: data.total_count,
|
||||
check_suites: data.check_suites.map(suite => ({
|
||||
id: suite.id,
|
||||
head_branch: suite.head_branch,
|
||||
head_sha: suite.head_sha,
|
||||
status: suite.status,
|
||||
conclusion: suite.conclusion,
|
||||
app: suite.app
|
||||
? {
|
||||
id: suite.app.id,
|
||||
slug: suite.app.slug,
|
||||
name: suite.app.name
|
||||
}
|
||||
: null,
|
||||
pull_requests: null, // Simplified for our use case
|
||||
created_at: suite.created_at,
|
||||
updated_at: suite.updated_at,
|
||||
latest_check_runs_count: suite.latest_check_runs_count
|
||||
}))
|
||||
};
|
||||
|
||||
return transformedResponse;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: err.message,
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
ref
|
||||
},
|
||||
'Failed to get check suites'
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove labels on a pull request
|
||||
*/
|
||||
export async function managePRLabels({
|
||||
repoOwner,
|
||||
repoName,
|
||||
prNumber,
|
||||
labelsToAdd = [],
|
||||
labelsToRemove = []
|
||||
}: ManagePRLabelsRequest): Promise<void> {
|
||||
try {
|
||||
// Validate parameters
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
@@ -526,11 +676,12 @@ async function managePRLabels({ repoOwner, repoName, prNumber, labelsToAdd = [],
|
||||
'Removed label from PR'
|
||||
);
|
||||
} catch (error) {
|
||||
const err = error as Error & { status?: number };
|
||||
// Ignore 404 errors (label not present)
|
||||
if (error.status !== 404) {
|
||||
if (err.status !== 404) {
|
||||
logger.error(
|
||||
{
|
||||
err: error.message,
|
||||
err: err.message,
|
||||
label
|
||||
},
|
||||
'Failed to remove label'
|
||||
@@ -557,9 +708,10 @@ async function managePRLabels({ repoOwner, repoName, prNumber, labelsToAdd = [],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: error.message,
|
||||
err: err.message,
|
||||
repo: `${repoOwner}/${repoName}`,
|
||||
pr: prNumber
|
||||
},
|
||||
@@ -568,13 +720,3 @@ async function managePRLabels({ repoOwner, repoName, prNumber, labelsToAdd = [],
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
postComment,
|
||||
addLabelsToIssue,
|
||||
createRepositoryLabels,
|
||||
getFallbackLabels,
|
||||
getCombinedStatus,
|
||||
hasReviewedPRAtCommit,
|
||||
managePRLabels
|
||||
};
|
||||
49
src/types.ts
Normal file
49
src/types.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// TypeScript type definitions for the claude-github-webhook project
|
||||
// This file establishes the TypeScript infrastructure
|
||||
|
||||
export interface GitHubWebhookPayload {
|
||||
action?: string;
|
||||
issue?: {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string;
|
||||
user: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
comment?: {
|
||||
id: number;
|
||||
body: string;
|
||||
user: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
repository?: {
|
||||
full_name: string;
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
pull_request?: {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string;
|
||||
user: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClaudeApiResponse {
|
||||
success: boolean;
|
||||
response?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ContainerExecutionOptions {
|
||||
command: string;
|
||||
repository: string;
|
||||
timeout?: number;
|
||||
environment?: Record<string, string>;
|
||||
}
|
||||
88
src/types/aws.ts
Normal file
88
src/types/aws.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
export interface AWSCredentials {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken?: string;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
export interface AWSProfile {
|
||||
name: string;
|
||||
region?: string;
|
||||
accessKeyId?: string;
|
||||
secretAccessKey?: string;
|
||||
roleArn?: string;
|
||||
sourceProfile?: string;
|
||||
mfaSerial?: string;
|
||||
externalId?: string;
|
||||
}
|
||||
|
||||
export interface AWSCredentialSource {
|
||||
type: 'profile' | 'instance' | 'task' | 'environment' | 'static';
|
||||
profileName?: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface AWSCredentialProviderOptions {
|
||||
profileName?: string;
|
||||
region?: string;
|
||||
timeout?: number;
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
export interface AWSCredentialProviderResult {
|
||||
credentials: AWSCredentials;
|
||||
source: AWSCredentialSource;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
export interface AWSInstanceMetadata {
|
||||
region: string;
|
||||
availabilityZone: string;
|
||||
instanceId: string;
|
||||
instanceType: string;
|
||||
localHostname: string;
|
||||
localIpv4: string;
|
||||
publicHostname?: string;
|
||||
publicIpv4?: string;
|
||||
}
|
||||
|
||||
export interface AWSTaskCredentials {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
export interface AWSCredentialError extends Error {
|
||||
code: string;
|
||||
statusCode?: number;
|
||||
retryable?: boolean;
|
||||
time?: Date;
|
||||
}
|
||||
|
||||
// Configuration types for AWS credential management
|
||||
export interface AWSCredentialConfig {
|
||||
defaultProfile?: string;
|
||||
credentialsFile?: string;
|
||||
configFile?: string;
|
||||
httpOptions?: {
|
||||
timeout?: number;
|
||||
connectTimeout?: number;
|
||||
};
|
||||
maxRetries?: number;
|
||||
retryDelayOptions?: {
|
||||
base?: number;
|
||||
customBackoff?: (retryCount: number) => number;
|
||||
};
|
||||
}
|
||||
|
||||
// Bedrock-specific types
|
||||
export interface BedrockConfig extends AWSCredentialConfig {
|
||||
region: string;
|
||||
model?: string;
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export interface BedrockCredentials extends AWSCredentials {
|
||||
region: string;
|
||||
}
|
||||
138
src/types/claude.ts
Normal file
138
src/types/claude.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
export type OperationType = 'auto-tagging' | 'pr-review' | 'manual-pr-review' | 'default';
|
||||
|
||||
export interface ClaudeCommandOptions {
|
||||
repoFullName: string;
|
||||
issueNumber: number | null;
|
||||
command: string;
|
||||
isPullRequest?: boolean;
|
||||
branchName?: string | null;
|
||||
operationType?: OperationType;
|
||||
}
|
||||
|
||||
export interface ClaudeProcessResult {
|
||||
success: boolean;
|
||||
response?: string;
|
||||
error?: string;
|
||||
errorReference?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface ClaudeContainerConfig {
|
||||
imageName: string;
|
||||
containerName: string;
|
||||
entrypointScript: string;
|
||||
privileged: boolean;
|
||||
capabilities: string[];
|
||||
resourceLimits: ClaudeResourceLimits;
|
||||
}
|
||||
|
||||
export interface ClaudeResourceLimits {
|
||||
memory: string;
|
||||
cpuShares: string;
|
||||
pidsLimit: string;
|
||||
}
|
||||
|
||||
export interface ClaudeEnvironmentVars {
|
||||
REPO_FULL_NAME: string;
|
||||
ISSUE_NUMBER: string;
|
||||
IS_PULL_REQUEST: string;
|
||||
BRANCH_NAME: string;
|
||||
OPERATION_TYPE: string;
|
||||
COMMAND: string;
|
||||
GITHUB_TOKEN: string;
|
||||
ANTHROPIC_API_KEY: string;
|
||||
BOT_USERNAME?: string;
|
||||
BOT_EMAIL?: string;
|
||||
}
|
||||
|
||||
export interface DockerExecutionOptions {
|
||||
maxBuffer: number;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export interface DockerExecutionResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
// Claude API Response Types
|
||||
export interface ClaudeAPIResponse {
|
||||
claudeResponse: string;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
context?: {
|
||||
repo: string;
|
||||
issue?: number;
|
||||
pr?: number;
|
||||
type: string;
|
||||
branch?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClaudeErrorResponse {
|
||||
success: false;
|
||||
error: string;
|
||||
errorReference?: string;
|
||||
timestamp?: string;
|
||||
message?: string;
|
||||
context?: {
|
||||
repo: string;
|
||||
issue?: number;
|
||||
pr?: number;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Container Security Configuration
|
||||
export interface ContainerCapabilities {
|
||||
NET_ADMIN: boolean;
|
||||
SYS_ADMIN: boolean;
|
||||
NET_RAW?: boolean;
|
||||
SYS_TIME?: boolean;
|
||||
DAC_OVERRIDE?: boolean;
|
||||
AUDIT_WRITE?: boolean;
|
||||
}
|
||||
|
||||
export interface ContainerSecurityConfig {
|
||||
privileged: boolean;
|
||||
requiredCapabilities: string[];
|
||||
optionalCapabilities: Record<string, boolean>;
|
||||
resourceLimits: ClaudeResourceLimits;
|
||||
}
|
||||
|
||||
// PR Review Types
|
||||
export interface PRReviewContext {
|
||||
prNumber: number;
|
||||
commitSha: string;
|
||||
repoFullName: string;
|
||||
branchName: string;
|
||||
}
|
||||
|
||||
export interface PRReviewResult {
|
||||
prNumber: number;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
skippedReason: string | null;
|
||||
}
|
||||
|
||||
// Auto-tagging Types
|
||||
export interface AutoTaggingContext {
|
||||
issueNumber: number;
|
||||
title: string;
|
||||
body: string | null;
|
||||
repoFullName: string;
|
||||
}
|
||||
|
||||
export interface LabelCategories {
|
||||
priority: string[];
|
||||
type: string[];
|
||||
complexity: string[];
|
||||
component: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_LABEL_CATEGORIES: LabelCategories = {
|
||||
priority: ['critical', 'high', 'medium', 'low'],
|
||||
type: ['bug', 'feature', 'enhancement', 'documentation', 'question', 'security'],
|
||||
complexity: ['trivial', 'simple', 'moderate', 'complex'],
|
||||
component: ['api', 'frontend', 'backend', 'database', 'auth', 'webhook', 'docker']
|
||||
};
|
||||
170
src/types/config.ts
Normal file
170
src/types/config.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// Environment variable configuration types
|
||||
export interface EnvironmentConfig {
|
||||
// Required environment variables
|
||||
BOT_USERNAME: string;
|
||||
BOT_EMAIL: string;
|
||||
GITHUB_WEBHOOK_SECRET: string;
|
||||
GITHUB_TOKEN: string;
|
||||
ANTHROPIC_API_KEY: string;
|
||||
|
||||
// Optional environment variables with defaults
|
||||
PORT?: string;
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
DEFAULT_AUTHORIZED_USER?: string;
|
||||
AUTHORIZED_USERS?: string;
|
||||
|
||||
// Claude container configuration
|
||||
CLAUDE_CONTAINER_IMAGE?: string;
|
||||
CLAUDE_CONTAINER_PRIVILEGED?: string;
|
||||
CLAUDE_CONTAINER_MEMORY_LIMIT?: string;
|
||||
CLAUDE_CONTAINER_CPU_SHARES?: string;
|
||||
CLAUDE_CONTAINER_PIDS_LIMIT?: string;
|
||||
CONTAINER_LIFETIME_MS?: string;
|
||||
|
||||
// Container capabilities
|
||||
CLAUDE_CONTAINER_CAP_NET_RAW?: string;
|
||||
CLAUDE_CONTAINER_CAP_SYS_TIME?: string;
|
||||
CLAUDE_CONTAINER_CAP_DAC_OVERRIDE?: string;
|
||||
CLAUDE_CONTAINER_CAP_AUDIT_WRITE?: string;
|
||||
|
||||
// PR review configuration
|
||||
PR_REVIEW_WAIT_FOR_ALL_CHECKS?: string;
|
||||
PR_REVIEW_TRIGGER_WORKFLOW?: string;
|
||||
PR_REVIEW_DEBOUNCE_MS?: string;
|
||||
PR_REVIEW_MAX_WAIT_MS?: string;
|
||||
PR_REVIEW_CONDITIONAL_TIMEOUT_MS?: string;
|
||||
|
||||
// Testing and development
|
||||
SKIP_WEBHOOK_VERIFICATION?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationConfig {
|
||||
// Server configuration
|
||||
port: number;
|
||||
nodeEnv: 'development' | 'production' | 'test';
|
||||
|
||||
// Bot configuration
|
||||
botUsername: string;
|
||||
botEmail: string;
|
||||
authorizedUsers: string[];
|
||||
|
||||
// GitHub configuration
|
||||
githubWebhookSecret: string;
|
||||
githubToken: string;
|
||||
skipWebhookVerification: boolean;
|
||||
|
||||
// Claude configuration
|
||||
anthropicApiKey: string;
|
||||
claudeContainerImage: string;
|
||||
containerLifetimeMs: number;
|
||||
|
||||
// Container security configuration
|
||||
container: {
|
||||
privileged: boolean;
|
||||
memoryLimit: string;
|
||||
cpuShares: string;
|
||||
pidsLimit: string;
|
||||
capabilities: {
|
||||
netRaw: boolean;
|
||||
sysTime: boolean;
|
||||
dacOverride: boolean;
|
||||
auditWrite: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// PR review configuration
|
||||
prReview: {
|
||||
waitForAllChecks: boolean;
|
||||
triggerWorkflow?: string;
|
||||
debounceMs: number;
|
||||
maxWaitMs: number;
|
||||
conditionalTimeoutMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Configuration validation
|
||||
export interface ConfigValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface RequiredEnvVar {
|
||||
name: keyof EnvironmentConfig;
|
||||
description: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface OptionalEnvVar extends RequiredEnvVar {
|
||||
defaultValue: string | number | boolean;
|
||||
}
|
||||
|
||||
// Security configuration
|
||||
export interface SecurityConfig {
|
||||
webhookSignatureRequired: boolean;
|
||||
rateLimiting: {
|
||||
enabled: boolean;
|
||||
windowMs: number;
|
||||
maxRequests: number;
|
||||
};
|
||||
cors: {
|
||||
enabled: boolean;
|
||||
origins: string[];
|
||||
};
|
||||
helmet: {
|
||||
enabled: boolean;
|
||||
options: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
// Logging configuration
|
||||
export interface LoggingConfig {
|
||||
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
format: 'json' | 'pretty';
|
||||
redaction: {
|
||||
enabled: boolean;
|
||||
patterns: string[];
|
||||
};
|
||||
file: {
|
||||
enabled: boolean;
|
||||
path?: string;
|
||||
maxSize?: string;
|
||||
maxFiles?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Performance monitoring configuration
|
||||
export interface MonitoringConfig {
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
endpoint?: string;
|
||||
interval?: number;
|
||||
};
|
||||
tracing: {
|
||||
enabled: boolean;
|
||||
sampleRate?: number;
|
||||
};
|
||||
healthCheck: {
|
||||
enabled: boolean;
|
||||
interval?: number;
|
||||
timeout?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Feature flags
|
||||
export interface FeatureFlags {
|
||||
autoTagging: boolean;
|
||||
prReview: boolean;
|
||||
containerIsolation: boolean;
|
||||
advancedSecurity: boolean;
|
||||
metricsCollection: boolean;
|
||||
}
|
||||
|
||||
// Complete application configuration
|
||||
export interface AppConfiguration {
|
||||
app: ApplicationConfig;
|
||||
security: SecurityConfig;
|
||||
logging: LoggingConfig;
|
||||
monitoring: MonitoringConfig;
|
||||
features: FeatureFlags;
|
||||
}
|
||||
29
src/types/environment.ts
Normal file
29
src/types/environment.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Environment variable access helpers to handle strict typing
|
||||
export function getEnvVar(key: string): string | undefined {
|
||||
return process.env[key];
|
||||
}
|
||||
|
||||
export function getRequiredEnvVar(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Required environment variable ${key} is not set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getEnvVarWithDefault(key: string, defaultValue: string): string {
|
||||
return process.env[key] ?? defaultValue;
|
||||
}
|
||||
|
||||
export function getBooleanEnvVar(key: string, defaultValue = false): boolean {
|
||||
const value = process.env[key];
|
||||
if (!value) return defaultValue;
|
||||
return value.toLowerCase() === 'true' || value === '1';
|
||||
}
|
||||
|
||||
export function getNumberEnvVar(key: string, defaultValue: number): number {
|
||||
const value = process.env[key];
|
||||
if (!value) return defaultValue;
|
||||
const parsed = parseInt(value, 10);
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
}
|
||||
150
src/types/express.ts
Normal file
150
src/types/express.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import type { GitHubWebhookPayload } from './github';
|
||||
import type { StartupMetrics } from './metrics';
|
||||
|
||||
// Extended Express Request with custom properties
|
||||
export interface WebhookRequest extends Request {
|
||||
rawBody?: Buffer;
|
||||
startupMetrics?: StartupMetrics;
|
||||
body: GitHubWebhookPayload;
|
||||
}
|
||||
|
||||
export interface ClaudeAPIRequest extends Request {
|
||||
body: {
|
||||
repoFullName?: string;
|
||||
repository?: string;
|
||||
issueNumber?: number;
|
||||
command: string;
|
||||
isPullRequest?: boolean;
|
||||
branchName?: string;
|
||||
authToken?: string;
|
||||
useContainer?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Custom response types for our endpoints
|
||||
export interface WebhookResponse {
|
||||
success?: boolean;
|
||||
message: string;
|
||||
context?: {
|
||||
repo: string;
|
||||
issue?: number;
|
||||
pr?: number;
|
||||
type?: string;
|
||||
sender?: string;
|
||||
branch?: string;
|
||||
};
|
||||
claudeResponse?: string;
|
||||
errorReference?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface HealthCheckResponse {
|
||||
status: 'ok' | 'degraded' | 'error';
|
||||
timestamp: string;
|
||||
startup?: StartupMetrics;
|
||||
docker: {
|
||||
available: boolean;
|
||||
error: string | null;
|
||||
checkTime: number | null;
|
||||
};
|
||||
claudeCodeImage: {
|
||||
available: boolean;
|
||||
error: string | null;
|
||||
checkTime: number | null;
|
||||
};
|
||||
healthCheckDuration?: number;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
message?: string;
|
||||
errorReference?: string;
|
||||
timestamp?: string;
|
||||
context?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Middleware types
|
||||
export type WebhookHandler = (
|
||||
req: WebhookRequest,
|
||||
res: Response<WebhookResponse | ErrorResponse>
|
||||
) =>
|
||||
| Promise<Response<WebhookResponse | ErrorResponse> | void>
|
||||
| Response<WebhookResponse | ErrorResponse>
|
||||
| void;
|
||||
|
||||
export type ClaudeAPIHandler = (
|
||||
req: ClaudeAPIRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => Promise<Response | void> | Response | void;
|
||||
|
||||
export type HealthCheckHandler = (
|
||||
req: Request,
|
||||
res: Response<HealthCheckResponse>,
|
||||
next: NextFunction
|
||||
) => Promise<void> | void;
|
||||
|
||||
export type ErrorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response<ErrorResponse>,
|
||||
next: NextFunction
|
||||
) => void;
|
||||
|
||||
// Request logging types
|
||||
export interface RequestLogData {
|
||||
method: string;
|
||||
url: string;
|
||||
statusCode: number;
|
||||
responseTime: string;
|
||||
}
|
||||
|
||||
export interface WebhookHeaders {
|
||||
'x-github-event'?: string;
|
||||
'x-github-delivery'?: string;
|
||||
'x-hub-signature-256'?: string;
|
||||
'user-agent'?: string;
|
||||
'content-type'?: string;
|
||||
}
|
||||
|
||||
// Express app configuration
|
||||
export interface AppConfig {
|
||||
port: number;
|
||||
bodyParserLimit?: string;
|
||||
requestTimeout?: number;
|
||||
rateLimitWindowMs?: number;
|
||||
rateLimitMax?: number;
|
||||
}
|
||||
|
||||
// Custom error types for Express handlers
|
||||
export interface ValidationError extends Error {
|
||||
statusCode: 400;
|
||||
field?: string;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export interface AuthenticationError extends Error {
|
||||
statusCode: 401;
|
||||
challenge?: string;
|
||||
}
|
||||
|
||||
export interface AuthorizationError extends Error {
|
||||
statusCode: 403;
|
||||
requiredPermission?: string;
|
||||
}
|
||||
|
||||
export interface NotFoundError extends Error {
|
||||
statusCode: 404;
|
||||
resource?: string;
|
||||
}
|
||||
|
||||
export interface WebhookVerificationError extends Error {
|
||||
statusCode: 401;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface RateLimitError extends Error {
|
||||
statusCode: 429;
|
||||
retryAfter?: number;
|
||||
}
|
||||
220
src/types/github.ts
Normal file
220
src/types/github.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
export interface GitHubWebhookPayload {
|
||||
action?: string;
|
||||
issue?: GitHubIssue;
|
||||
pull_request?: GitHubPullRequest;
|
||||
comment?: GitHubComment;
|
||||
check_suite?: GitHubCheckSuite;
|
||||
repository: GitHubRepository;
|
||||
sender: GitHubUser;
|
||||
}
|
||||
|
||||
export interface GitHubIssue {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string | null;
|
||||
state: 'open' | 'closed';
|
||||
user: GitHubUser;
|
||||
labels: GitHubLabel[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
html_url: string;
|
||||
pull_request?: {
|
||||
head?: {
|
||||
ref: string;
|
||||
sha: string;
|
||||
};
|
||||
base?: {
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubPullRequest {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string | null;
|
||||
state: 'open' | 'closed' | 'merged';
|
||||
user: GitHubUser;
|
||||
head: GitHubPullRequestHead;
|
||||
base: GitHubPullRequestBase;
|
||||
labels: GitHubLabel[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
html_url: string;
|
||||
merged: boolean;
|
||||
mergeable: boolean | null;
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubPullRequestHead {
|
||||
ref: string;
|
||||
sha: string;
|
||||
repo: GitHubRepository | null;
|
||||
}
|
||||
|
||||
export interface GitHubPullRequestBase {
|
||||
ref: string;
|
||||
sha: string;
|
||||
repo: GitHubRepository;
|
||||
}
|
||||
|
||||
export interface GitHubComment {
|
||||
id: number;
|
||||
body: string;
|
||||
user: GitHubUser;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubCheckSuite {
|
||||
id: number;
|
||||
head_branch: string | null;
|
||||
head_sha: string;
|
||||
status: 'queued' | 'in_progress' | 'completed' | 'pending' | 'waiting' | 'requested' | null;
|
||||
conclusion:
|
||||
| 'success'
|
||||
| 'failure'
|
||||
| 'neutral'
|
||||
| 'cancelled'
|
||||
| 'skipped'
|
||||
| 'timed_out'
|
||||
| 'action_required'
|
||||
| 'startup_failure'
|
||||
| 'stale'
|
||||
| null;
|
||||
app: GitHubApp | null;
|
||||
pull_requests: GitHubPullRequest[] | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
latest_check_runs_count: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GitHubApp {
|
||||
id: number;
|
||||
slug?: string;
|
||||
name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GitHubRepository {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
owner: GitHubUser;
|
||||
private: boolean;
|
||||
html_url: string;
|
||||
default_branch: string;
|
||||
}
|
||||
|
||||
export interface GitHubUser {
|
||||
id: number;
|
||||
login: string;
|
||||
type: 'User' | 'Bot' | 'Organization';
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubLabel {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface GitHubCombinedStatus {
|
||||
state: string;
|
||||
total_count: number;
|
||||
statuses: GitHubStatus[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GitHubStatus {
|
||||
state: string;
|
||||
context: string;
|
||||
description: string | null;
|
||||
target_url: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GitHubCheckSuitesResponse {
|
||||
total_count: number;
|
||||
check_suites: GitHubCheckSuite[];
|
||||
}
|
||||
|
||||
export interface GitHubReview {
|
||||
id: number;
|
||||
user: GitHubUser;
|
||||
body: string | null;
|
||||
state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | 'PENDING';
|
||||
html_url: string;
|
||||
commit_id: string;
|
||||
submitted_at: string | null;
|
||||
}
|
||||
|
||||
// API Request/Response Types
|
||||
export interface CreateCommentRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
issueNumber: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface CreateCommentResponse {
|
||||
id: number | string;
|
||||
body: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AddLabelsRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
issueNumber: number;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface ManagePRLabelsRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
prNumber: number;
|
||||
labelsToAdd?: string[];
|
||||
labelsToRemove?: string[];
|
||||
}
|
||||
|
||||
export interface CreateLabelRequest {
|
||||
name: string;
|
||||
color: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateRepositoryLabelsRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
labels: CreateLabelRequest[];
|
||||
}
|
||||
|
||||
export interface GetCombinedStatusRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
ref: string;
|
||||
}
|
||||
|
||||
export interface HasReviewedPRRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
prNumber: number;
|
||||
commitSha: string;
|
||||
}
|
||||
|
||||
export interface GetCheckSuitesRequest {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
ref: string;
|
||||
}
|
||||
|
||||
// Validation Types
|
||||
export interface ValidatedGitHubParams {
|
||||
repoOwner: string;
|
||||
repoName: string;
|
||||
issueNumber: number;
|
||||
}
|
||||
62
src/types/index.ts
Normal file
62
src/types/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Central export file for all types
|
||||
export * from './github';
|
||||
export * from './claude';
|
||||
export * from './aws';
|
||||
export * from './express';
|
||||
export * from './config';
|
||||
export * from './metrics';
|
||||
|
||||
// Common utility types
|
||||
export interface BaseResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
timestamp: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
// Import types for type guards and aliases
|
||||
import type { GitHubWebhookPayload } from './github';
|
||||
import type { ClaudeCommandOptions } from './claude';
|
||||
import type { AWSCredentials } from './aws';
|
||||
import type { ApplicationConfig } from './config';
|
||||
import type { PerformanceMetrics } from './metrics';
|
||||
|
||||
// Type guards for runtime type checking
|
||||
export function isWebhookPayload(obj: unknown): obj is GitHubWebhookPayload {
|
||||
return typeof obj === 'object' && obj !== null && 'repository' in obj && 'sender' in obj;
|
||||
}
|
||||
|
||||
export function isClaudeCommandOptions(obj: unknown): obj is ClaudeCommandOptions {
|
||||
return typeof obj === 'object' && obj !== null && 'repoFullName' in obj && 'command' in obj;
|
||||
}
|
||||
|
||||
export function isAWSCredentials(obj: unknown): obj is AWSCredentials {
|
||||
return (
|
||||
typeof obj === 'object' && obj !== null && 'accessKeyId' in obj && 'secretAccessKey' in obj
|
||||
);
|
||||
}
|
||||
|
||||
// Common type aliases for convenience
|
||||
export type WebhookPayload = GitHubWebhookPayload;
|
||||
export type ClaudeOptions = ClaudeCommandOptions;
|
||||
export type AWSCreds = AWSCredentials;
|
||||
export type AppConfig = ApplicationConfig;
|
||||
export type Metrics = PerformanceMetrics;
|
||||
165
src/types/metrics.ts
Normal file
165
src/types/metrics.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
// Performance metrics and monitoring types
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export interface StartupMilestone {
|
||||
name: string;
|
||||
timestamp: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface StartupMetrics {
|
||||
startTime: number;
|
||||
milestones: StartupMilestone[];
|
||||
ready: boolean;
|
||||
totalStartupTime?: number;
|
||||
|
||||
// Methods (when implemented as a class)
|
||||
recordMilestone(name: string, description?: string): void;
|
||||
markReady(): number;
|
||||
metricsMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
|
||||
}
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
requestCount: number;
|
||||
averageResponseTime: number;
|
||||
errorRate: number;
|
||||
uptime: number;
|
||||
memoryUsage: {
|
||||
used: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
};
|
||||
cpuUsage: {
|
||||
user: number;
|
||||
system: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequestMetrics {
|
||||
method: string;
|
||||
path: string;
|
||||
statusCode: number;
|
||||
responseTime: number;
|
||||
timestamp: number;
|
||||
userAgent?: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
export interface DockerMetrics {
|
||||
containerCount: number;
|
||||
imageCount: number;
|
||||
volumeCount: number;
|
||||
networkCount: number;
|
||||
systemInfo: {
|
||||
kernelVersion: string;
|
||||
operatingSystem: string;
|
||||
architecture: string;
|
||||
totalMemory: number;
|
||||
cpus: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClaudeExecutionMetrics {
|
||||
totalExecutions: number;
|
||||
successfulExecutions: number;
|
||||
failedExecutions: number;
|
||||
averageExecutionTime: number;
|
||||
containerStartupTime: number;
|
||||
operationTypes: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface GitHubAPIMetrics {
|
||||
totalRequests: number;
|
||||
rateLimitRemaining: number;
|
||||
rateLimitResetTime: number;
|
||||
requestsByEndpoint: Record<string, number>;
|
||||
errorsByType: Record<string, number>;
|
||||
}
|
||||
|
||||
// Health check types
|
||||
export interface HealthStatus {
|
||||
status: 'healthy' | 'unhealthy' | 'degraded';
|
||||
timestamp: string;
|
||||
uptime: number;
|
||||
version?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface ComponentHealth {
|
||||
name: string;
|
||||
status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
lastChecked: string;
|
||||
responseTime?: number;
|
||||
error?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DetailedHealthCheck extends HealthStatus {
|
||||
components: ComponentHealth[];
|
||||
metrics: PerformanceMetrics;
|
||||
dependencies: {
|
||||
github: ComponentHealth;
|
||||
claude: ComponentHealth;
|
||||
docker: ComponentHealth;
|
||||
database?: ComponentHealth;
|
||||
};
|
||||
}
|
||||
|
||||
// Monitoring and alerting
|
||||
export interface AlertThreshold {
|
||||
metric: string;
|
||||
operator: 'gt' | 'lt' | 'eq' | 'gte' | 'lte';
|
||||
value: number;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
}
|
||||
|
||||
export interface MetricAlert {
|
||||
id: string;
|
||||
threshold: AlertThreshold;
|
||||
currentValue: number;
|
||||
triggered: boolean;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MetricsCollector {
|
||||
// Core metrics collection
|
||||
recordRequest(metrics: RequestMetrics): void;
|
||||
recordClaudeExecution(success: boolean, duration: number, operationType: string): void;
|
||||
recordGitHubAPICall(endpoint: string, success: boolean, rateLimitRemaining?: number): void;
|
||||
|
||||
// Health monitoring
|
||||
checkComponentHealth(componentName: string): Promise<ComponentHealth>;
|
||||
getOverallHealth(): Promise<DetailedHealthCheck>;
|
||||
|
||||
// Metrics retrieval
|
||||
getMetrics(): PerformanceMetrics;
|
||||
getStartupMetrics(): StartupMetrics;
|
||||
|
||||
// Alerting
|
||||
checkThresholds(): MetricAlert[];
|
||||
addThreshold(threshold: AlertThreshold): void;
|
||||
removeThreshold(id: string): void;
|
||||
}
|
||||
|
||||
// Time series data for metrics
|
||||
export interface TimeSeriesDataPoint {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface TimeSeries {
|
||||
metric: string;
|
||||
dataPoints: TimeSeriesDataPoint[];
|
||||
resolution: 'second' | 'minute' | 'hour' | 'day';
|
||||
}
|
||||
|
||||
export interface MetricsSnapshot {
|
||||
timestamp: string;
|
||||
performance: PerformanceMetrics;
|
||||
claude: ClaudeExecutionMetrics;
|
||||
github: GitHubAPIMetrics;
|
||||
docker: DockerMetrics;
|
||||
timeSeries: TimeSeries[];
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
const { createLogger } = require('./logger');
|
||||
|
||||
const logger = createLogger('awsCredentialProvider');
|
||||
|
||||
/**
|
||||
* AWS Credential Provider for secure credential management
|
||||
* Implements best practices for AWS authentication
|
||||
*/
|
||||
class AWSCredentialProvider {
|
||||
constructor() {
|
||||
this.credentials = null;
|
||||
this.expirationTime = null;
|
||||
this.credentialSource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AWS credentials - PROFILES ONLY
|
||||
*
|
||||
* This method implements a caching mechanism to avoid repeatedly reading
|
||||
* credential files. It checks for cached credentials first, and only reads
|
||||
* from the filesystem if necessary.
|
||||
*
|
||||
* The cached credentials are cleared when:
|
||||
* 1. clearCache() is called explicitly
|
||||
* 2. When credentials expire (for temporary credentials)
|
||||
*
|
||||
* Static credentials from profiles don't expire, so they remain cached
|
||||
* until the process ends or cache is explicitly cleared.
|
||||
*
|
||||
* @returns {Promise<Object>} Credential object with accessKeyId, secretAccessKey, and region
|
||||
* @throws {Error} If AWS_PROFILE is not set or credential retrieval fails
|
||||
*/
|
||||
async getCredentials() {
|
||||
if (!process.env.AWS_PROFILE) {
|
||||
throw new Error('AWS_PROFILE must be set. Direct credential passing is not supported.');
|
||||
}
|
||||
|
||||
// Return cached credentials if available and not expired
|
||||
if (this.credentials && !this.isExpired()) {
|
||||
logger.info('Using cached credentials');
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
logger.info('Using AWS profile authentication only');
|
||||
|
||||
try {
|
||||
this.credentials = await this.getProfileCredentials(process.env.AWS_PROFILE);
|
||||
this.credentialSource = `AWS Profile (${process.env.AWS_PROFILE})`;
|
||||
return this.credentials;
|
||||
} catch (error) {
|
||||
logger.error({ error: error.message }, 'Failed to get AWS credentials from profile');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials have expired
|
||||
*/
|
||||
isExpired() {
|
||||
if (!this.expirationTime) {
|
||||
return false; // Static credentials don't expire
|
||||
}
|
||||
return Date.now() > this.expirationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running on EC2 instance
|
||||
*/
|
||||
async isEC2Instance() {
|
||||
try {
|
||||
const response = await fetch('http://169.254.169.254/latest/meta-data/', {
|
||||
timeout: 1000
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials from EC2 instance metadata
|
||||
*/
|
||||
async getInstanceMetadataCredentials() {
|
||||
const tokenResponse = await fetch('http://169.254.169.254/latest/api/token', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-aws-ec2-metadata-token-ttl-seconds': '21600'
|
||||
},
|
||||
timeout: 1000
|
||||
});
|
||||
|
||||
const token = await tokenResponse.text();
|
||||
|
||||
const roleResponse = await fetch(
|
||||
'http://169.254.169.254/latest/meta-data/iam/security-credentials/',
|
||||
{
|
||||
headers: {
|
||||
'X-aws-ec2-metadata-token': token
|
||||
},
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
|
||||
const roleName = await roleResponse.text();
|
||||
|
||||
const credentialsResponse = await fetch(
|
||||
`http://169.254.169.254/latest/meta-data/iam/security-credentials/${roleName}`,
|
||||
{
|
||||
headers: {
|
||||
'X-aws-ec2-metadata-token': token
|
||||
},
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
|
||||
const credentials = await credentialsResponse.json();
|
||||
|
||||
this.expirationTime = new Date(credentials.Expiration).getTime();
|
||||
|
||||
return {
|
||||
accessKeyId: credentials.AccessKeyId,
|
||||
secretAccessKey: credentials.SecretAccessKey,
|
||||
sessionToken: credentials.Token,
|
||||
region: process.env.AWS_REGION
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials from ECS container metadata
|
||||
*/
|
||||
async getECSCredentials() {
|
||||
const uri = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI;
|
||||
const response = await fetch(`http://169.254.170.2${uri}`, {
|
||||
timeout: 1000
|
||||
});
|
||||
|
||||
const credentials = await response.json();
|
||||
|
||||
this.expirationTime = new Date(credentials.Expiration).getTime();
|
||||
|
||||
return {
|
||||
accessKeyId: credentials.AccessKeyId,
|
||||
secretAccessKey: credentials.SecretAccessKey,
|
||||
sessionToken: credentials.Token,
|
||||
region: process.env.AWS_REGION
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials from AWS profile
|
||||
*/
|
||||
async getProfileCredentials(profileName) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const credentialsPath = path.join(os.homedir(), '.aws', 'credentials');
|
||||
const configPath = path.join(os.homedir(), '.aws', 'config');
|
||||
|
||||
try {
|
||||
// Read credentials file
|
||||
const credentialsContent = fs.readFileSync(credentialsPath, 'utf8');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
|
||||
// Parse credentials for the specific profile
|
||||
const profileRegex = new RegExp(`\\[${profileName}\\]([^\\[]*)`);
|
||||
const credentialsMatch = credentialsContent.match(profileRegex);
|
||||
const configMatch = configContent.match(new RegExp(`\\[profile ${profileName}\\]([^\\[]*)`));
|
||||
|
||||
if (!credentialsMatch && !configMatch) {
|
||||
throw new Error(`Profile '${profileName}' not found`);
|
||||
}
|
||||
|
||||
const credentialsSection = credentialsMatch ? credentialsMatch[1] : '';
|
||||
const configSection = configMatch ? configMatch[1] : '';
|
||||
|
||||
// Extract credentials
|
||||
const accessKeyMatch = credentialsSection.match(/aws_access_key_id\s*=\s*(.+)/);
|
||||
const secretKeyMatch = credentialsSection.match(/aws_secret_access_key\s*=\s*(.+)/);
|
||||
const regionMatch = configSection.match(/region\s*=\s*(.+)/);
|
||||
|
||||
if (!accessKeyMatch || !secretKeyMatch) {
|
||||
throw new Error(`Incomplete credentials for profile '${profileName}'`);
|
||||
}
|
||||
|
||||
return {
|
||||
accessKeyId: accessKeyMatch[1].trim(),
|
||||
secretAccessKey: secretKeyMatch[1].trim(),
|
||||
region: regionMatch ? regionMatch[1].trim() : process.env.AWS_REGION
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({ error: error.message, profile: profileName }, 'Failed to read AWS profile');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variables for Docker container
|
||||
* PROFILES ONLY - No credential passing through environment variables
|
||||
*/
|
||||
async getDockerEnvVars() {
|
||||
if (!process.env.AWS_PROFILE) {
|
||||
throw new Error('AWS_PROFILE must be set. Direct credential passing is not supported.');
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
profile: process.env.AWS_PROFILE
|
||||
},
|
||||
'Using AWS profile authentication only'
|
||||
);
|
||||
|
||||
return {
|
||||
AWS_PROFILE: process.env.AWS_PROFILE,
|
||||
AWS_REGION: process.env.AWS_REGION
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached credentials (useful for testing or rotation)
|
||||
*/
|
||||
clearCache() {
|
||||
this.credentials = null;
|
||||
this.expirationTime = null;
|
||||
this.credentialSource = null;
|
||||
logger.info('Cleared credential cache');
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
module.exports = new AWSCredentialProvider();
|
||||
325
src/utils/awsCredentialProvider.ts
Normal file
325
src/utils/awsCredentialProvider.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/* global AbortSignal */
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { createLogger } from './logger';
|
||||
import type { AWSCredentials, AWSCredentialProviderResult, AWSCredentialError } from '../types/aws';
|
||||
|
||||
const logger = createLogger('awsCredentialProvider');
|
||||
|
||||
/**
|
||||
* AWS Credential Provider for secure credential management
|
||||
* Implements best practices for AWS authentication
|
||||
*/
|
||||
class AWSCredentialProvider {
|
||||
private credentials: AWSCredentials | null = null;
|
||||
private expirationTime: number | null = null;
|
||||
private credentialSource: string | null = null;
|
||||
|
||||
/**
|
||||
* Get AWS credentials - PROFILES ONLY
|
||||
*
|
||||
* This method implements a caching mechanism to avoid repeatedly reading
|
||||
* credential files. It checks for cached credentials first, and only reads
|
||||
* from the filesystem if necessary.
|
||||
*
|
||||
* The cached credentials are cleared when:
|
||||
* 1. clearCache() is called explicitly
|
||||
* 2. When credentials expire (for temporary credentials)
|
||||
*
|
||||
* Static credentials from profiles don't expire, so they remain cached
|
||||
* until the process ends or cache is explicitly cleared.
|
||||
*
|
||||
* @throws {AWSCredentialError} If AWS_PROFILE is not set or credential retrieval fails
|
||||
*/
|
||||
async getCredentials(): Promise<AWSCredentialProviderResult> {
|
||||
if (!process.env['AWS_PROFILE']) {
|
||||
const error = new Error(
|
||||
'AWS_PROFILE must be set. Direct credential passing is not supported.'
|
||||
) as AWSCredentialError;
|
||||
error.code = 'MISSING_PROFILE';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Return cached credentials if available and not expired
|
||||
if (this.credentials && !this.isExpired()) {
|
||||
logger.info('Using cached credentials');
|
||||
return {
|
||||
credentials: this.credentials,
|
||||
source: {
|
||||
type: 'profile',
|
||||
profileName: process.env['AWS_PROFILE'],
|
||||
isDefault: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
logger.info('Using AWS profile authentication only');
|
||||
|
||||
try {
|
||||
this.credentials = await this.getProfileCredentials(process.env['AWS_PROFILE']);
|
||||
this.credentialSource = `AWS Profile (${process.env['AWS_PROFILE']})`;
|
||||
|
||||
return {
|
||||
credentials: this.credentials,
|
||||
source: {
|
||||
type: 'profile',
|
||||
profileName: process.env['AWS_PROFILE'],
|
||||
isDefault: false
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const awsError = error as AWSCredentialError;
|
||||
awsError.code = awsError.code || 'PROFILE_ERROR';
|
||||
logger.error({ error: awsError.message }, 'Failed to get AWS credentials from profile');
|
||||
throw awsError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials have expired
|
||||
*/
|
||||
private isExpired(): boolean {
|
||||
if (!this.expirationTime) {
|
||||
return false; // Static credentials don't expire
|
||||
}
|
||||
return Date.now() > this.expirationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running on EC2 instance
|
||||
*/
|
||||
async isEC2Instance(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('http://169.254.169.254/latest/meta-data/', {
|
||||
signal: AbortSignal.timeout(1000)
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials from EC2 instance metadata
|
||||
*/
|
||||
async getInstanceMetadataCredentials(): Promise<AWSCredentials> {
|
||||
try {
|
||||
const tokenResponse = await fetch('http://169.254.169.254/latest/api/token', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-aws-ec2-metadata-token-ttl-seconds': '21600'
|
||||
},
|
||||
signal: AbortSignal.timeout(1000)
|
||||
});
|
||||
|
||||
const token = await tokenResponse.text();
|
||||
|
||||
const roleResponse = await fetch(
|
||||
'http://169.254.169.254/latest/meta-data/iam/security-credentials/',
|
||||
{
|
||||
headers: {
|
||||
'X-aws-ec2-metadata-token': token
|
||||
},
|
||||
signal: AbortSignal.timeout(1000)
|
||||
}
|
||||
);
|
||||
|
||||
const roleName = await roleResponse.text();
|
||||
|
||||
const credentialsResponse = await fetch(
|
||||
`http://169.254.169.254/latest/meta-data/iam/security-credentials/${roleName}`,
|
||||
{
|
||||
headers: {
|
||||
'X-aws-ec2-metadata-token': token
|
||||
},
|
||||
signal: AbortSignal.timeout(1000)
|
||||
}
|
||||
);
|
||||
|
||||
const credentials = (await credentialsResponse.json()) as {
|
||||
AccessKeyId: string;
|
||||
SecretAccessKey: string;
|
||||
Token: string;
|
||||
Expiration: string;
|
||||
};
|
||||
|
||||
this.expirationTime = new Date(credentials.Expiration).getTime();
|
||||
|
||||
return {
|
||||
accessKeyId: credentials.AccessKeyId,
|
||||
secretAccessKey: credentials.SecretAccessKey,
|
||||
sessionToken: credentials.Token,
|
||||
region: process.env.AWS_REGION
|
||||
};
|
||||
} catch (error) {
|
||||
const awsError = new Error(
|
||||
`Failed to get EC2 instance credentials: ${error}`
|
||||
) as AWSCredentialError;
|
||||
awsError.code = 'EC2_METADATA_ERROR';
|
||||
throw awsError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials from ECS container metadata
|
||||
*/
|
||||
async getECSCredentials(): Promise<AWSCredentials> {
|
||||
const uri = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI;
|
||||
if (!uri) {
|
||||
const error = new Error(
|
||||
'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI not set'
|
||||
) as AWSCredentialError;
|
||||
error.code = 'MISSING_ECS_URI';
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://169.254.170.2${uri}`, {
|
||||
signal: AbortSignal.timeout(1000)
|
||||
});
|
||||
|
||||
const credentials = (await response.json()) as {
|
||||
AccessKeyId: string;
|
||||
SecretAccessKey: string;
|
||||
Token: string;
|
||||
Expiration: string;
|
||||
};
|
||||
|
||||
this.expirationTime = new Date(credentials.Expiration).getTime();
|
||||
|
||||
return {
|
||||
accessKeyId: credentials.AccessKeyId,
|
||||
secretAccessKey: credentials.SecretAccessKey,
|
||||
sessionToken: credentials.Token,
|
||||
region: process.env.AWS_REGION
|
||||
};
|
||||
} catch (error) {
|
||||
const awsError = new Error(`Failed to get ECS credentials: ${error}`) as AWSCredentialError;
|
||||
awsError.code = 'ECS_METADATA_ERROR';
|
||||
throw awsError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials from AWS profile
|
||||
*/
|
||||
private async getProfileCredentials(profileName: string): Promise<AWSCredentials> {
|
||||
const credentialsPath = path.join(os.homedir(), '.aws', 'credentials');
|
||||
const configPath = path.join(os.homedir(), '.aws', 'config');
|
||||
|
||||
try {
|
||||
// Read credentials file
|
||||
const credentialsContent = await fs.readFile(credentialsPath, 'utf8');
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
|
||||
// Parse credentials for the specific profile (escape profile name to prevent regex injection)
|
||||
const escapedProfileName = profileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const profileRegex = new RegExp(`\\[${escapedProfileName}\\]([^\\[]*)`);
|
||||
const credentialsMatch = credentialsContent.match(profileRegex);
|
||||
const configMatch = configContent.match(
|
||||
new RegExp(`\\[profile ${escapedProfileName}\\]([^\\[]*)`)
|
||||
);
|
||||
|
||||
if (!credentialsMatch && !configMatch) {
|
||||
const error = new Error(`Profile '${profileName}' not found`) as AWSCredentialError;
|
||||
error.code = 'PROFILE_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const credentialsSection = credentialsMatch ? credentialsMatch[1] : '';
|
||||
const configSection = configMatch ? configMatch[1] : '';
|
||||
|
||||
// Extract credentials
|
||||
const accessKeyMatch = credentialsSection.match(/aws_access_key_id\s*=\s*(.+)/);
|
||||
const secretKeyMatch = credentialsSection.match(/aws_secret_access_key\s*=\s*(.+)/);
|
||||
const regionMatch = configSection.match(/region\s*=\s*(.+)/);
|
||||
|
||||
if (!accessKeyMatch || !secretKeyMatch) {
|
||||
const error = new Error(
|
||||
`Incomplete credentials for profile '${profileName}'`
|
||||
) as AWSCredentialError;
|
||||
error.code = 'INCOMPLETE_CREDENTIALS';
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
accessKeyId: accessKeyMatch[1].trim(),
|
||||
secretAccessKey: secretKeyMatch[1].trim(),
|
||||
region: regionMatch ? regionMatch[1].trim() : process.env.AWS_REGION
|
||||
};
|
||||
} catch (error) {
|
||||
const awsError = error as AWSCredentialError;
|
||||
if (!awsError.code) {
|
||||
awsError.code = 'PROFILE_READ_ERROR';
|
||||
}
|
||||
logger.error({ error: awsError.message, profile: profileName }, 'Failed to read AWS profile');
|
||||
throw awsError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variables for Docker container
|
||||
* PROFILES ONLY - No credential passing through environment variables
|
||||
*/
|
||||
getDockerEnvVars(): Record<string, string | undefined> {
|
||||
if (!process.env.AWS_PROFILE) {
|
||||
const error = new Error(
|
||||
'AWS_PROFILE must be set. Direct credential passing is not supported.'
|
||||
) as AWSCredentialError;
|
||||
error.code = 'MISSING_PROFILE';
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
profile: process.env.AWS_PROFILE
|
||||
},
|
||||
'Using AWS profile authentication only'
|
||||
);
|
||||
|
||||
return {
|
||||
AWS_PROFILE: process.env.AWS_PROFILE,
|
||||
AWS_REGION: process.env.AWS_REGION
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached credentials (useful for testing or rotation)
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.credentials = null;
|
||||
this.expirationTime = null;
|
||||
this.credentialSource = null;
|
||||
logger.info('Cleared credential cache');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current credential source information
|
||||
*/
|
||||
getCredentialSource(): string | null {
|
||||
return this.credentialSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached credentials without fetching new ones
|
||||
*/
|
||||
getCachedCredentials(): AWSCredentials | null {
|
||||
if (this.credentials && !this.isExpired()) {
|
||||
return this.credentials;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials are currently cached and valid
|
||||
*/
|
||||
hasCachedCredentials(): boolean {
|
||||
return this.credentials !== null && !this.isExpired();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
const awsCredentialProvider = new AWSCredentialProvider();
|
||||
export default awsCredentialProvider;
|
||||
export { AWSCredentialProvider };
|
||||
@@ -1,156 +0,0 @@
|
||||
const pino = require('pino');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
// Use home directory for logs to avoid permission issues
|
||||
const homeDir = process.env.HOME || '/tmp';
|
||||
const logsDir = path.join(homeDir, '.claude-webhook', 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Determine if we should use file transport in production
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const logFileName = path.join(logsDir, 'app.log');
|
||||
|
||||
// Configure different transports based on environment
|
||||
const transport = isProduction
|
||||
? {
|
||||
targets: [
|
||||
// File transport for production
|
||||
{
|
||||
target: 'pino/file',
|
||||
options: { destination: logFileName, mkdir: true }
|
||||
},
|
||||
// Console pretty transport
|
||||
{
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: true,
|
||||
translateTime: 'SYS:standard'
|
||||
},
|
||||
level: 'info'
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
// Just use pretty logs in development
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: true,
|
||||
translateTime: 'SYS:standard'
|
||||
}
|
||||
};
|
||||
|
||||
// Configure the logger
|
||||
const logger = pino({
|
||||
transport,
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
// Include the hostname and pid in the log data
|
||||
base: {
|
||||
pid: process.pid,
|
||||
hostname: process.env.HOSTNAME || 'unknown',
|
||||
env: process.env.NODE_ENV || 'development'
|
||||
},
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
// Define custom log levels if needed
|
||||
customLevels: {
|
||||
http: 35 // Between info (30) and debug (20)
|
||||
},
|
||||
redact: {
|
||||
paths: [
|
||||
'headers.authorization',
|
||||
'*.password',
|
||||
'*.token',
|
||||
'*.secret',
|
||||
'*.secretKey',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'GITHUB_TOKEN',
|
||||
'GH_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'*.AWS_SECRET_ACCESS_KEY',
|
||||
'*.AWS_ACCESS_KEY_ID',
|
||||
'*.GITHUB_TOKEN',
|
||||
'*.GH_TOKEN',
|
||||
'*.ANTHROPIC_API_KEY',
|
||||
'dockerCommand',
|
||||
'*.dockerCommand',
|
||||
'envVars.AWS_SECRET_ACCESS_KEY',
|
||||
'envVars.AWS_ACCESS_KEY_ID',
|
||||
'envVars.GITHUB_TOKEN',
|
||||
'envVars.GH_TOKEN',
|
||||
'envVars.ANTHROPIC_API_KEY',
|
||||
'env.AWS_SECRET_ACCESS_KEY',
|
||||
'env.AWS_ACCESS_KEY_ID',
|
||||
'env.GITHUB_TOKEN',
|
||||
'env.GH_TOKEN',
|
||||
'env.ANTHROPIC_API_KEY',
|
||||
'stderr',
|
||||
'*.stderr',
|
||||
'stdout',
|
||||
'*.stdout',
|
||||
'error.dockerCommand',
|
||||
'error.stderr',
|
||||
'error.stdout',
|
||||
'process.env.GITHUB_TOKEN',
|
||||
'process.env.GH_TOKEN',
|
||||
'process.env.ANTHROPIC_API_KEY',
|
||||
'process.env.AWS_SECRET_ACCESS_KEY',
|
||||
'process.env.AWS_ACCESS_KEY_ID'
|
||||
],
|
||||
censor: '[REDACTED]'
|
||||
}
|
||||
});
|
||||
|
||||
// Add simple file rotation (will be replaced with pino-roll in production)
|
||||
if (isProduction) {
|
||||
// Check log file size and rotate if necessary
|
||||
try {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (fs.existsSync(logFileName)) {
|
||||
const stats = fs.statSync(logFileName);
|
||||
if (stats.size > maxSize) {
|
||||
// Simple rotation - keep up to 5 backup files
|
||||
for (let i = 4; i >= 0; i--) {
|
||||
const oldFile = `${logFileName}.${i}`;
|
||||
const newFile = `${logFileName}.${i + 1}`;
|
||||
if (fs.existsSync(oldFile)) {
|
||||
fs.renameSync(oldFile, newFile);
|
||||
}
|
||||
}
|
||||
fs.renameSync(logFileName, `${logFileName}.0`);
|
||||
|
||||
logger.info('Log file rotated');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Error rotating log file');
|
||||
}
|
||||
}
|
||||
|
||||
// Log startup message
|
||||
logger.info(
|
||||
{
|
||||
app: 'claude-github-webhook',
|
||||
startTime: new Date().toISOString(),
|
||||
nodeVersion: process.version,
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
logLevel: logger.level
|
||||
},
|
||||
'Application starting'
|
||||
);
|
||||
|
||||
// Create a child logger for specific components
|
||||
const createLogger = component => {
|
||||
return logger.child({ component });
|
||||
};
|
||||
|
||||
// Export the logger factory
|
||||
module.exports = {
|
||||
logger,
|
||||
createLogger
|
||||
};
|
||||
424
src/utils/logger.ts
Normal file
424
src/utils/logger.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import pino from 'pino';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
// Use home directory for logs to avoid permission issues
|
||||
const homeDir = process.env['HOME'] ?? '/tmp';
|
||||
const logsDir = path.join(homeDir, '.claude-webhook', 'logs');
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
// eslint-disable-next-line no-sync
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Determine if we should use file transport in production
|
||||
const isProduction = process.env['NODE_ENV'] === 'production';
|
||||
const logFileName = path.join(logsDir, 'app.log');
|
||||
|
||||
// Configure different transports based on environment
|
||||
const transport = isProduction
|
||||
? {
|
||||
targets: [
|
||||
// File transport for production
|
||||
{
|
||||
target: 'pino/file',
|
||||
options: { destination: logFileName, mkdir: true }
|
||||
},
|
||||
// Console pretty transport
|
||||
{
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: true,
|
||||
translateTime: 'SYS:standard'
|
||||
},
|
||||
level: 'info'
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
// Just use pretty logs in development
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: true,
|
||||
translateTime: 'SYS:standard'
|
||||
}
|
||||
};
|
||||
|
||||
// Configure the logger
|
||||
const logger = pino({
|
||||
transport,
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
// Include the hostname and pid in the log data
|
||||
base: {
|
||||
pid: process.pid,
|
||||
hostname: process.env['HOSTNAME'] ?? 'unknown',
|
||||
env: process.env['NODE_ENV'] ?? 'development'
|
||||
},
|
||||
level: process.env['LOG_LEVEL'] ?? 'info',
|
||||
// Define custom log levels if needed
|
||||
customLevels: {
|
||||
http: 35 // Between info (30) and debug (20)
|
||||
},
|
||||
redact: {
|
||||
paths: [
|
||||
// HTTP headers that might contain credentials
|
||||
'headers.authorization',
|
||||
'headers["x-api-key"]',
|
||||
'headers["x-auth-token"]',
|
||||
'headers["x-github-token"]',
|
||||
'headers.bearer',
|
||||
'*.headers.authorization',
|
||||
'*.headers["x-api-key"]',
|
||||
'*.headers["x-auth-token"]',
|
||||
'*.headers["x-github-token"]',
|
||||
'*.headers.bearer',
|
||||
|
||||
// Generic sensitive field patterns (top-level)
|
||||
'password',
|
||||
'passwd',
|
||||
'pass',
|
||||
'token',
|
||||
'secret',
|
||||
'secretKey',
|
||||
'secret_key',
|
||||
'apiKey',
|
||||
'api_key',
|
||||
'credential',
|
||||
'credentials',
|
||||
'key',
|
||||
'private',
|
||||
'privateKey',
|
||||
'private_key',
|
||||
'auth',
|
||||
'authentication',
|
||||
|
||||
// Generic sensitive field patterns (nested)
|
||||
'*.password',
|
||||
'*.passwd',
|
||||
'*.pass',
|
||||
'*.token',
|
||||
'*.secret',
|
||||
'*.secretKey',
|
||||
'*.secret_key',
|
||||
'*.apiKey',
|
||||
'*.api_key',
|
||||
'*.credential',
|
||||
'*.credentials',
|
||||
'*.key',
|
||||
'*.private',
|
||||
'*.privateKey',
|
||||
'*.private_key',
|
||||
'*.auth',
|
||||
'*.authentication',
|
||||
|
||||
// Specific environment variables (top-level)
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SESSION_TOKEN',
|
||||
'AWS_SECURITY_TOKEN',
|
||||
'GITHUB_TOKEN',
|
||||
'GH_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'GITHUB_WEBHOOK_SECRET',
|
||||
'WEBHOOK_SECRET',
|
||||
'BOT_TOKEN',
|
||||
'API_KEY',
|
||||
'SECRET_KEY',
|
||||
'ACCESS_TOKEN',
|
||||
'REFRESH_TOKEN',
|
||||
'JWT_SECRET',
|
||||
'DATABASE_URL',
|
||||
'DB_PASSWORD',
|
||||
'REDIS_PASSWORD',
|
||||
|
||||
// Nested in any object (*)
|
||||
'*.AWS_SECRET_ACCESS_KEY',
|
||||
'*.AWS_ACCESS_KEY_ID',
|
||||
'*.AWS_SESSION_TOKEN',
|
||||
'*.AWS_SECURITY_TOKEN',
|
||||
'*.GITHUB_TOKEN',
|
||||
'*.GH_TOKEN',
|
||||
'*.ANTHROPIC_API_KEY',
|
||||
'*.GITHUB_WEBHOOK_SECRET',
|
||||
'*.WEBHOOK_SECRET',
|
||||
'*.BOT_TOKEN',
|
||||
'*.API_KEY',
|
||||
'*.SECRET_KEY',
|
||||
'*.ACCESS_TOKEN',
|
||||
'*.REFRESH_TOKEN',
|
||||
'*.JWT_SECRET',
|
||||
'*.DATABASE_URL',
|
||||
'*.DB_PASSWORD',
|
||||
'*.REDIS_PASSWORD',
|
||||
|
||||
// Docker-related sensitive content
|
||||
'dockerCommand',
|
||||
'*.dockerCommand',
|
||||
'dockerArgs',
|
||||
'*.dockerArgs',
|
||||
'command',
|
||||
'*.command',
|
||||
|
||||
// Environment variable containers
|
||||
'envVars.AWS_SECRET_ACCESS_KEY',
|
||||
'envVars.AWS_ACCESS_KEY_ID',
|
||||
'envVars.AWS_SESSION_TOKEN',
|
||||
'envVars.AWS_SECURITY_TOKEN',
|
||||
'envVars.GITHUB_TOKEN',
|
||||
'envVars.GH_TOKEN',
|
||||
'envVars.ANTHROPIC_API_KEY',
|
||||
'envVars.GITHUB_WEBHOOK_SECRET',
|
||||
'envVars.WEBHOOK_SECRET',
|
||||
'envVars.BOT_TOKEN',
|
||||
'envVars.API_KEY',
|
||||
'envVars.SECRET_KEY',
|
||||
'envVars.ACCESS_TOKEN',
|
||||
'envVars.REFRESH_TOKEN',
|
||||
'envVars.JWT_SECRET',
|
||||
'envVars.DATABASE_URL',
|
||||
'envVars.DB_PASSWORD',
|
||||
'envVars.REDIS_PASSWORD',
|
||||
|
||||
'env.AWS_SECRET_ACCESS_KEY',
|
||||
'env.AWS_ACCESS_KEY_ID',
|
||||
'env.AWS_SESSION_TOKEN',
|
||||
'env.AWS_SECURITY_TOKEN',
|
||||
'env.GITHUB_TOKEN',
|
||||
'env.GH_TOKEN',
|
||||
'env.ANTHROPIC_API_KEY',
|
||||
'env.GITHUB_WEBHOOK_SECRET',
|
||||
'env.WEBHOOK_SECRET',
|
||||
'env.BOT_TOKEN',
|
||||
'env.API_KEY',
|
||||
'env.SECRET_KEY',
|
||||
'env.ACCESS_TOKEN',
|
||||
'env.REFRESH_TOKEN',
|
||||
'env.JWT_SECRET',
|
||||
'env.DATABASE_URL',
|
||||
'env.DB_PASSWORD',
|
||||
'env.REDIS_PASSWORD',
|
||||
|
||||
// Process environment variables (using bracket notation for nested objects)
|
||||
'process["env"]["AWS_SECRET_ACCESS_KEY"]',
|
||||
'process["env"]["AWS_ACCESS_KEY_ID"]',
|
||||
'process["env"]["AWS_SESSION_TOKEN"]',
|
||||
'process["env"]["AWS_SECURITY_TOKEN"]',
|
||||
'process["env"]["GITHUB_TOKEN"]',
|
||||
'process["env"]["GH_TOKEN"]',
|
||||
'process["env"]["ANTHROPIC_API_KEY"]',
|
||||
'process["env"]["GITHUB_WEBHOOK_SECRET"]',
|
||||
'process["env"]["WEBHOOK_SECRET"]',
|
||||
'process["env"]["BOT_TOKEN"]',
|
||||
'process["env"]["API_KEY"]',
|
||||
'process["env"]["SECRET_KEY"]',
|
||||
'process["env"]["ACCESS_TOKEN"]',
|
||||
'process["env"]["REFRESH_TOKEN"]',
|
||||
'process["env"]["JWT_SECRET"]',
|
||||
'process["env"]["DATABASE_URL"]',
|
||||
'process["env"]["DB_PASSWORD"]',
|
||||
'process["env"]["REDIS_PASSWORD"]',
|
||||
|
||||
// Process environment variables (as top-level bracket notation keys)
|
||||
'["process.env.AWS_SECRET_ACCESS_KEY"]',
|
||||
'["process.env.AWS_ACCESS_KEY_ID"]',
|
||||
'["process.env.AWS_SESSION_TOKEN"]',
|
||||
'["process.env.AWS_SECURITY_TOKEN"]',
|
||||
'["process.env.GITHUB_TOKEN"]',
|
||||
'["process.env.GH_TOKEN"]',
|
||||
'["process.env.ANTHROPIC_API_KEY"]',
|
||||
'["process.env.GITHUB_WEBHOOK_SECRET"]',
|
||||
'["process.env.WEBHOOK_SECRET"]',
|
||||
'["process.env.BOT_TOKEN"]',
|
||||
'["process.env.API_KEY"]',
|
||||
'["process.env.SECRET_KEY"]',
|
||||
'["process.env.ACCESS_TOKEN"]',
|
||||
'["process.env.REFRESH_TOKEN"]',
|
||||
'["process.env.JWT_SECRET"]',
|
||||
'["process.env.DATABASE_URL"]',
|
||||
'["process.env.DB_PASSWORD"]',
|
||||
'["process.env.REDIS_PASSWORD"]',
|
||||
|
||||
// Output streams that might contain leaked credentials
|
||||
'stderr',
|
||||
'*.stderr',
|
||||
'stdout',
|
||||
'*.stdout',
|
||||
'output',
|
||||
'*.output',
|
||||
'logs',
|
||||
'*.logs',
|
||||
'message',
|
||||
'*.message',
|
||||
'data',
|
||||
'*.data',
|
||||
|
||||
// Error objects that might contain sensitive information
|
||||
'error.dockerCommand',
|
||||
'error.stderr',
|
||||
'error.stdout',
|
||||
'error.output',
|
||||
'error.message',
|
||||
'error.data',
|
||||
'err.dockerCommand',
|
||||
'err.stderr',
|
||||
'err.stdout',
|
||||
'err.output',
|
||||
'err.message',
|
||||
'err.data',
|
||||
|
||||
// HTTP request/response objects
|
||||
'request.headers.authorization',
|
||||
'response.headers.authorization',
|
||||
'req.headers.authorization',
|
||||
'res.headers.authorization',
|
||||
'*.request.headers.authorization',
|
||||
'*.response.headers.authorization',
|
||||
'*.req.headers.authorization',
|
||||
'*.res.headers.authorization',
|
||||
|
||||
// File paths that might contain credentials
|
||||
'credentialsPath',
|
||||
'*.credentialsPath',
|
||||
'keyPath',
|
||||
'*.keyPath',
|
||||
'secretPath',
|
||||
'*.secretPath',
|
||||
|
||||
// Database connection strings and configurations
|
||||
'connectionString',
|
||||
'*.connectionString',
|
||||
'dbUrl',
|
||||
'*.dbUrl',
|
||||
'mongoUrl',
|
||||
'*.mongoUrl',
|
||||
'redisUrl',
|
||||
'*.redisUrl',
|
||||
|
||||
// Authentication objects
|
||||
'auth.token',
|
||||
'auth.secret',
|
||||
'auth.key',
|
||||
'auth.password',
|
||||
'*.auth.token',
|
||||
'*.auth.secret',
|
||||
'*.auth.key',
|
||||
'*.auth.password',
|
||||
'authentication.token',
|
||||
'authentication.secret',
|
||||
'authentication.key',
|
||||
'authentication.password',
|
||||
'*.authentication.token',
|
||||
'*.authentication.secret',
|
||||
'*.authentication.key',
|
||||
'*.authentication.password',
|
||||
|
||||
// Deep nested patterns (up to 4 levels deep)
|
||||
'*.*.password',
|
||||
'*.*.secret',
|
||||
'*.*.token',
|
||||
'*.*.apiKey',
|
||||
'*.*.api_key',
|
||||
'*.*.credential',
|
||||
'*.*.key',
|
||||
'*.*.privateKey',
|
||||
'*.*.private_key',
|
||||
'*.*.AWS_SECRET_ACCESS_KEY',
|
||||
'*.*.AWS_ACCESS_KEY_ID',
|
||||
'*.*.GITHUB_TOKEN',
|
||||
'*.*.ANTHROPIC_API_KEY',
|
||||
'*.*.connectionString',
|
||||
'*.*.DATABASE_URL',
|
||||
|
||||
'*.*.*.password',
|
||||
'*.*.*.secret',
|
||||
'*.*.*.token',
|
||||
'*.*.*.apiKey',
|
||||
'*.*.*.api_key',
|
||||
'*.*.*.credential',
|
||||
'*.*.*.key',
|
||||
'*.*.*.privateKey',
|
||||
'*.*.*.private_key',
|
||||
'*.*.*.AWS_SECRET_ACCESS_KEY',
|
||||
'*.*.*.AWS_ACCESS_KEY_ID',
|
||||
'*.*.*.GITHUB_TOKEN',
|
||||
'*.*.*.ANTHROPIC_API_KEY',
|
||||
'*.*.*.connectionString',
|
||||
'*.*.*.DATABASE_URL',
|
||||
|
||||
'*.*.*.*.password',
|
||||
'*.*.*.*.secret',
|
||||
'*.*.*.*.token',
|
||||
'*.*.*.*.apiKey',
|
||||
'*.*.*.*.api_key',
|
||||
'*.*.*.*.credential',
|
||||
'*.*.*.*.key',
|
||||
'*.*.*.*.privateKey',
|
||||
'*.*.*.*.private_key',
|
||||
'*.*.*.*.AWS_SECRET_ACCESS_KEY',
|
||||
'*.*.*.*.AWS_ACCESS_KEY_ID',
|
||||
'*.*.*.*.GITHUB_TOKEN',
|
||||
'*.*.*.*.ANTHROPIC_API_KEY',
|
||||
'*.*.*.*.connectionString',
|
||||
'*.*.*.*.DATABASE_URL'
|
||||
],
|
||||
censor: process.env.DISABLE_LOG_REDACTION ? undefined : '[REDACTED]'
|
||||
}
|
||||
});
|
||||
|
||||
// Add simple file rotation (will be replaced with pino-roll in production)
|
||||
if (isProduction) {
|
||||
// Check log file size and rotate if necessary
|
||||
try {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
if (fs.existsSync(logFileName)) {
|
||||
// eslint-disable-next-line no-sync
|
||||
const stats = fs.statSync(logFileName);
|
||||
if (stats.size > maxSize) {
|
||||
// Simple rotation - keep up to 5 backup files
|
||||
for (let i = 4; i >= 0; i--) {
|
||||
const oldFile = `${logFileName}.${i}`;
|
||||
const newFile = `${logFileName}.${i + 1}`;
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
if (fs.existsSync(oldFile)) {
|
||||
// eslint-disable-next-line no-sync
|
||||
fs.renameSync(oldFile, newFile);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-sync
|
||||
fs.renameSync(logFileName, `${logFileName}.0`);
|
||||
|
||||
logger.info('Log file rotated');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Error rotating log file');
|
||||
}
|
||||
}
|
||||
|
||||
// Log startup message
|
||||
logger.info(
|
||||
{
|
||||
app: 'claude-github-webhook',
|
||||
startTime: new Date().toISOString(),
|
||||
nodeVersion: process.version,
|
||||
env: process.env['NODE_ENV'] ?? 'development',
|
||||
logLevel: logger.level
|
||||
},
|
||||
'Application starting'
|
||||
);
|
||||
|
||||
// Create a child logger for specific components
|
||||
const createLogger = (component: string): pino.Logger => {
|
||||
return logger.child({ component }) as unknown as pino.Logger;
|
||||
};
|
||||
|
||||
// Export the logger factory with proper typing
|
||||
export { logger, createLogger };
|
||||
export type Logger = pino.Logger;
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Utilities for sanitizing text to prevent infinite loops and other issues
|
||||
*/
|
||||
const { createLogger } = require('./logger');
|
||||
const logger = createLogger('sanitize');
|
||||
|
||||
/**
|
||||
* Sanitizes text to prevent infinite loops by removing bot username mentions
|
||||
* @param {string} text - The text to sanitize
|
||||
* @returns {string} - Sanitized text
|
||||
*/
|
||||
function sanitizeBotMentions(text) {
|
||||
if (!text) return text;
|
||||
|
||||
// Get bot username from environment variables - required
|
||||
const BOT_USERNAME = process.env.BOT_USERNAME;
|
||||
|
||||
if (!BOT_USERNAME) {
|
||||
logger.warn('BOT_USERNAME environment variable is not set. Cannot sanitize properly.');
|
||||
return text;
|
||||
}
|
||||
|
||||
// Create a regex to find all bot username mentions
|
||||
// First escape any special regex characters
|
||||
const escapedUsername = BOT_USERNAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Look for the username with @ symbol anywhere in the text
|
||||
const botMentionRegex = new RegExp(escapedUsername, 'gi');
|
||||
|
||||
// Replace mentions with a sanitized version (remove @ symbol if present)
|
||||
const sanitizedName = BOT_USERNAME.startsWith('@') ? BOT_USERNAME.substring(1) : BOT_USERNAME;
|
||||
const sanitized = text.replace(botMentionRegex, sanitizedName);
|
||||
|
||||
// If sanitization occurred, log it
|
||||
if (sanitized !== text) {
|
||||
logger.warn('Sanitized bot mentions from text to prevent infinite loops');
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an array of labels to remove potentially sensitive or invalid characters.
|
||||
* @param {string[]} labels - The array of labels to sanitize.
|
||||
* @returns {string[]} - The sanitized array of labels.
|
||||
*/
|
||||
function sanitizeLabels(labels) {
|
||||
return labels.map(label => label.replace(/[^a-zA-Z0-9:_-]/g, ''));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sanitizeBotMentions,
|
||||
sanitizeLabels
|
||||
};
|
||||
103
src/utils/sanitize.ts
Normal file
103
src/utils/sanitize.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const logger = createLogger('sanitize');
|
||||
|
||||
/**
|
||||
* Sanitizes text to prevent infinite loops by removing bot username mentions
|
||||
*/
|
||||
export function sanitizeBotMentions(text: string): string {
|
||||
if (!text) return text;
|
||||
|
||||
// Get bot username from environment variables - required
|
||||
const BOT_USERNAME = process.env['BOT_USERNAME'];
|
||||
|
||||
if (!BOT_USERNAME) {
|
||||
logger.warn('BOT_USERNAME environment variable is not set. Cannot sanitize properly.');
|
||||
return text;
|
||||
}
|
||||
|
||||
// Create a regex to find all bot username mentions
|
||||
// First escape any special regex characters
|
||||
const escapedUsername = BOT_USERNAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Look for the username with @ symbol anywhere in the text
|
||||
const botMentionRegex = new RegExp(escapedUsername, 'gi');
|
||||
|
||||
// Replace mentions with a sanitized version (remove @ symbol if present)
|
||||
const sanitizedName = BOT_USERNAME.startsWith('@') ? BOT_USERNAME.substring(1) : BOT_USERNAME;
|
||||
const sanitized = text.replace(botMentionRegex, sanitizedName);
|
||||
|
||||
// If sanitization occurred, log it
|
||||
if (sanitized !== text) {
|
||||
logger.warn('Sanitized bot mentions from text to prevent infinite loops');
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an array of labels to remove potentially sensitive or invalid characters
|
||||
*/
|
||||
export function sanitizeLabels(labels: string[]): string[] {
|
||||
return labels.map(label => label.replace(/[^a-zA-Z0-9:_-]/g, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes input for safe usage in commands and prevents injection attacks
|
||||
*/
|
||||
export function sanitizeCommandInput(input: string): string {
|
||||
if (!input) return input;
|
||||
|
||||
// Remove or escape potentially dangerous characters
|
||||
return input
|
||||
.replace(/[`$\\]/g, '') // Remove backticks, dollar signs, and backslashes
|
||||
.replace(/[;&|><]/g, '') // Remove command injection characters
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a string contains only safe repository name characters
|
||||
*/
|
||||
export function validateRepositoryName(name: string): boolean {
|
||||
const repoPattern = /^[a-zA-Z0-9._-]+$/;
|
||||
return repoPattern.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a string contains only safe GitHub reference characters
|
||||
*/
|
||||
export function validateGitHubRef(ref: string): boolean {
|
||||
// GitHub refs cannot:
|
||||
// - be empty
|
||||
// - contain consecutive dots (..)
|
||||
// - contain spaces or special characters like @ or #
|
||||
if (!ref || ref.includes('..') || ref.includes(' ') || ref.includes('@') || ref.includes('#')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must contain only allowed characters
|
||||
const refPattern = /^[a-zA-Z0-9._/-]+$/;
|
||||
return refPattern.test(ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes environment variable values for logging
|
||||
*/
|
||||
export function sanitizeEnvironmentValue(key: string, value: string): string {
|
||||
const sensitiveKeys = [
|
||||
'TOKEN',
|
||||
'SECRET',
|
||||
'KEY',
|
||||
'PASSWORD',
|
||||
'CREDENTIAL',
|
||||
'GITHUB_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'WEBHOOK_SECRET'
|
||||
];
|
||||
|
||||
const isSensitive = sensitiveKeys.some(sensitiveKey => key.toUpperCase().includes(sensitiveKey));
|
||||
|
||||
return isSensitive ? '[REDACTED]' : value;
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
const fs = require('fs');
|
||||
const { logger } = require('./logger');
|
||||
import fs from 'fs';
|
||||
import { logger } from './logger';
|
||||
|
||||
interface CredentialConfig {
|
||||
file: string;
|
||||
env: string;
|
||||
}
|
||||
|
||||
interface CredentialMappings {
|
||||
[key: string]: CredentialConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure credential loader - reads from files instead of env vars
|
||||
* Files are mounted as Docker secrets or regular files
|
||||
*/
|
||||
class SecureCredentials {
|
||||
private credentials: Map<string, string>;
|
||||
|
||||
constructor() {
|
||||
this.credentials = new Map();
|
||||
this.loadCredentials();
|
||||
@@ -14,38 +25,41 @@ class SecureCredentials {
|
||||
/**
|
||||
* Load credentials from files or fallback to env vars
|
||||
*/
|
||||
loadCredentials() {
|
||||
const credentialMappings = {
|
||||
private loadCredentials(): void {
|
||||
const credentialMappings: CredentialMappings = {
|
||||
GITHUB_TOKEN: {
|
||||
file: process.env.GITHUB_TOKEN_FILE || '/run/secrets/github_token',
|
||||
file: process.env['GITHUB_TOKEN_FILE'] ?? '/run/secrets/github_token',
|
||||
env: 'GITHUB_TOKEN'
|
||||
},
|
||||
ANTHROPIC_API_KEY: {
|
||||
file: process.env.ANTHROPIC_API_KEY_FILE || '/run/secrets/anthropic_api_key',
|
||||
file: process.env['ANTHROPIC_API_KEY_FILE'] ?? '/run/secrets/anthropic_api_key',
|
||||
env: 'ANTHROPIC_API_KEY'
|
||||
},
|
||||
GITHUB_WEBHOOK_SECRET: {
|
||||
file: process.env.GITHUB_WEBHOOK_SECRET_FILE || '/run/secrets/webhook_secret',
|
||||
file: process.env['GITHUB_WEBHOOK_SECRET_FILE'] ?? '/run/secrets/webhook_secret',
|
||||
env: 'GITHUB_WEBHOOK_SECRET'
|
||||
}
|
||||
};
|
||||
|
||||
for (const [key, config] of Object.entries(credentialMappings)) {
|
||||
let value = null;
|
||||
let value: string | null = null;
|
||||
|
||||
// Try to read from file first (most secure)
|
||||
try {
|
||||
// eslint-disable-next-line no-sync
|
||||
if (fs.existsSync(config.file)) {
|
||||
// eslint-disable-next-line no-sync
|
||||
value = fs.readFileSync(config.file, 'utf8').trim();
|
||||
logger.info(`Loaded ${key} from secure file: ${config.file}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to read ${key} from file ${config.file}: ${error.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.warn(`Failed to read ${key} from file ${config.file}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
// Fallback to environment variable (less secure)
|
||||
if (!value && process.env[config.env]) {
|
||||
value = process.env[config.env];
|
||||
value = process.env[config.env] as string;
|
||||
logger.warn(`Using ${key} from environment variable (less secure)`);
|
||||
}
|
||||
|
||||
@@ -59,41 +73,63 @@ class SecureCredentials {
|
||||
|
||||
/**
|
||||
* Get credential value
|
||||
* @param {string} key - Credential key
|
||||
* @returns {string|null} - Credential value or null if not found
|
||||
*/
|
||||
get(key) {
|
||||
return this.credentials.get(key) || null;
|
||||
get(key: string): string | null {
|
||||
return this.credentials.get(key) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credential exists
|
||||
* @param {string} key - Credential key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(key) {
|
||||
has(key: string): boolean {
|
||||
return this.credentials.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available credential keys (for debugging)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getAvailableKeys() {
|
||||
getAvailableKeys(): string[] {
|
||||
return Array.from(this.credentials.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload credentials (useful for credential rotation)
|
||||
*/
|
||||
reload() {
|
||||
reload(): void {
|
||||
this.credentials.clear();
|
||||
this.loadCredentials();
|
||||
logger.info('Credentials reloaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a credential programmatically
|
||||
*/
|
||||
set(key: string, value: string): void {
|
||||
this.credentials.set(key, value);
|
||||
logger.debug(`Credential ${key} updated programmatically`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a credential
|
||||
*/
|
||||
delete(key: string): boolean {
|
||||
const deleted = this.credentials.delete(key);
|
||||
if (deleted) {
|
||||
logger.debug(`Credential ${key} removed`);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credential count
|
||||
*/
|
||||
size(): number {
|
||||
return this.credentials.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const secureCredentials = new SecureCredentials();
|
||||
|
||||
module.exports = secureCredentials;
|
||||
export default secureCredentials;
|
||||
export { SecureCredentials };
|
||||
@@ -1,66 +0,0 @@
|
||||
const { createLogger } = require('./logger');
|
||||
|
||||
class StartupMetrics {
|
||||
constructor() {
|
||||
this.logger = createLogger('startup-metrics');
|
||||
this.startTime = Date.now();
|
||||
this.milestones = {};
|
||||
this.isReady = false;
|
||||
}
|
||||
|
||||
recordMilestone(name, description = '') {
|
||||
const timestamp = Date.now();
|
||||
const elapsed = timestamp - this.startTime;
|
||||
|
||||
this.milestones[name] = {
|
||||
timestamp,
|
||||
elapsed,
|
||||
description
|
||||
};
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
milestone: name,
|
||||
elapsed: `${elapsed}ms`,
|
||||
description
|
||||
},
|
||||
`Startup milestone: ${name}`
|
||||
);
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
markReady() {
|
||||
const totalTime = this.recordMilestone('service_ready', 'Service is ready to accept requests');
|
||||
this.isReady = true;
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
totalStartupTime: `${totalTime}ms`,
|
||||
milestones: this.milestones
|
||||
},
|
||||
'Service startup completed'
|
||||
);
|
||||
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return {
|
||||
isReady: this.isReady,
|
||||
totalElapsed: Date.now() - this.startTime,
|
||||
milestones: this.milestones,
|
||||
startTime: this.startTime
|
||||
};
|
||||
}
|
||||
|
||||
// Middleware to add startup metrics to responses
|
||||
metricsMiddleware() {
|
||||
return (req, res, next) => {
|
||||
req.startupMetrics = this.getMetrics();
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { StartupMetrics };
|
||||
129
src/utils/startup-metrics.ts
Normal file
129
src/utils/startup-metrics.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { createLogger } from './logger';
|
||||
import type { StartupMilestone, StartupMetrics as IStartupMetrics } from '../types/metrics';
|
||||
|
||||
interface MilestoneData {
|
||||
timestamp: number;
|
||||
elapsed: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface MilestonesMap {
|
||||
[name: string]: MilestoneData;
|
||||
}
|
||||
|
||||
export class StartupMetrics implements IStartupMetrics {
|
||||
private logger = createLogger('startup-metrics');
|
||||
public readonly startTime: number;
|
||||
public milestones: StartupMilestone[] = [];
|
||||
private milestonesMap: MilestonesMap = {};
|
||||
public ready = false;
|
||||
public totalStartupTime?: number;
|
||||
|
||||
constructor() {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
recordMilestone(name: string, description = ''): void {
|
||||
const timestamp = Date.now();
|
||||
const elapsed = timestamp - this.startTime;
|
||||
|
||||
const milestone: StartupMilestone = {
|
||||
name,
|
||||
timestamp,
|
||||
description
|
||||
};
|
||||
|
||||
// Store in both array and map for different access patterns
|
||||
this.milestones.push(milestone);
|
||||
this.milestonesMap[name] = {
|
||||
timestamp,
|
||||
elapsed,
|
||||
description
|
||||
};
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
milestone: name,
|
||||
elapsed: `${elapsed}ms`,
|
||||
description
|
||||
},
|
||||
`Startup milestone: ${name}`
|
||||
);
|
||||
}
|
||||
|
||||
markReady(): number {
|
||||
const timestamp = Date.now();
|
||||
const totalTime = timestamp - this.startTime;
|
||||
|
||||
this.recordMilestone('service_ready', 'Service is ready to accept requests');
|
||||
this.ready = true;
|
||||
this.totalStartupTime = totalTime;
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
totalStartupTime: `${totalTime}ms`,
|
||||
milestones: this.milestonesMap
|
||||
},
|
||||
'Service startup completed'
|
||||
);
|
||||
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
getMetrics(): StartupMetricsResponse {
|
||||
return {
|
||||
isReady: this.ready,
|
||||
totalElapsed: Date.now() - this.startTime,
|
||||
milestones: this.milestonesMap,
|
||||
startTime: this.startTime,
|
||||
totalStartupTime: this.totalStartupTime ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
// Middleware to add startup metrics to responses
|
||||
metricsMiddleware() {
|
||||
return (
|
||||
req: Request & { startupMetrics?: StartupMetricsResponse },
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
req.startupMetrics = this.getMetrics();
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Additional utility methods for TypeScript implementation
|
||||
getMilestone(name: string): MilestoneData | undefined {
|
||||
return this.milestonesMap[name];
|
||||
}
|
||||
|
||||
getMilestoneNames(): string[] {
|
||||
return Object.keys(this.milestonesMap);
|
||||
}
|
||||
|
||||
getElapsedTime(): number {
|
||||
return Date.now() - this.startTime;
|
||||
}
|
||||
|
||||
isServiceReady(): boolean {
|
||||
return this.ready;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.milestones = [];
|
||||
this.milestonesMap = {};
|
||||
this.ready = false;
|
||||
delete this.totalStartupTime;
|
||||
this.logger.info('Startup metrics reset');
|
||||
}
|
||||
}
|
||||
|
||||
// Response interface for metrics
|
||||
interface StartupMetricsResponse {
|
||||
isReady: boolean;
|
||||
totalElapsed: number;
|
||||
milestones: MilestonesMap;
|
||||
startTime: number;
|
||||
totalStartupTime?: number;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="jest tests" tests="38" failures="0" errors="0" time="0.646">
|
||||
<testsuite name="Claude Service" errors="0" failures="0" skipped="0" timestamp="2025-05-24T18:17:16" time="0.346" tests="4">
|
||||
<testcase classname="Claude Service processCommand should handle test mode correctly" name="Claude Service processCommand should handle test mode correctly" time="0.003">
|
||||
</testcase>
|
||||
<testcase classname="Claude Service processCommand should properly set up Docker command in production mode" name="Claude Service processCommand should properly set up Docker command in production mode" time="0.002">
|
||||
</testcase>
|
||||
<testcase classname="Claude Service processCommand should handle errors properly" name="Claude Service processCommand should handle errors properly" time="0.014">
|
||||
</testcase>
|
||||
<testcase classname="Claude Service processCommand should write long commands to temp files" name="Claude Service processCommand should write long commands to temp files" time="0.001">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="GitHub Controller - Check Suite Events" errors="0" failures="0" skipped="2" timestamp="2025-05-24T18:17:16" time="0.072" tests="10">
|
||||
<testcase classname="GitHub Controller - Check Suite Events should trigger PR review when check suite succeeds with PRs and combined status passes" name="GitHub Controller - Check Suite Events should trigger PR review when check suite succeeds with PRs and combined status passes" time="0.004">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should not trigger PR review when check suite fails" name="GitHub Controller - Check Suite Events should not trigger PR review when check suite fails" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should not trigger PR review when check suite succeeds but has no PRs" name="GitHub Controller - Check Suite Events should not trigger PR review when check suite succeeds but has no PRs" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should handle multiple PRs in check suite in parallel" name="GitHub Controller - Check Suite Events should handle multiple PRs in check suite in parallel" time="0.002">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should handle Claude service errors gracefully" name="GitHub Controller - Check Suite Events should handle Claude service errors gracefully" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should skip PR when head.sha is missing" name="GitHub Controller - Check Suite Events should skip PR when head.sha is missing" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should skip PR review when combined status is not success" name="GitHub Controller - Check Suite Events should skip PR review when combined status is not success" time="0">
|
||||
<skipped/>
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should handle combined status API errors" name="GitHub Controller - Check Suite Events should handle combined status API errors" time="0">
|
||||
<skipped/>
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should handle mixed success and failure in multiple PRs" name="GitHub Controller - Check Suite Events should handle mixed success and failure in multiple PRs" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller - Check Suite Events should skip PR review when already reviewed at same commit" name="GitHub Controller - Check Suite Events should skip PR review when already reviewed at same commit" time="0">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="githubService" errors="0" failures="0" skipped="0" timestamp="2025-05-24T18:17:16" time="0.064" tests="10">
|
||||
<testcase classname="githubService getFallbackLabels should identify bug labels correctly" name="githubService getFallbackLabels should identify bug labels correctly" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="githubService getFallbackLabels should identify feature labels correctly" name="githubService getFallbackLabels should identify feature labels correctly" time="0">
|
||||
</testcase>
|
||||
<testcase classname="githubService getFallbackLabels should identify enhancement labels correctly" name="githubService getFallbackLabels should identify enhancement labels correctly" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="githubService getFallbackLabels should identify question labels correctly" name="githubService getFallbackLabels should identify question labels correctly" time="0">
|
||||
</testcase>
|
||||
<testcase classname="githubService getFallbackLabels should identify documentation labels correctly" name="githubService getFallbackLabels should identify documentation labels correctly" time="0">
|
||||
</testcase>
|
||||
<testcase classname="githubService getFallbackLabels should default to medium priority when no specific priority keywords found" name="githubService getFallbackLabels should default to medium priority when no specific priority keywords found" time="0">
|
||||
</testcase>
|
||||
<testcase classname="githubService getFallbackLabels should handle empty descriptions gracefully" name="githubService getFallbackLabels should handle empty descriptions gracefully" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="githubService addLabelsToIssue - test mode should return mock data in test mode" name="githubService addLabelsToIssue - test mode should return mock data in test mode" time="0">
|
||||
</testcase>
|
||||
<testcase classname="githubService createRepositoryLabels - test mode should return labels array in test mode" name="githubService createRepositoryLabels - test mode should return labels array in test mode" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="githubService postComment - test mode should return mock comment data in test mode" name="githubService postComment - test mode should return mock comment data in test mode" time="0">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="AWS Credential Provider" errors="0" failures="0" skipped="0" timestamp="2025-05-24T18:17:16" time="0.036" tests="7">
|
||||
<testcase classname="AWS Credential Provider should get credentials from AWS profile" name="AWS Credential Provider should get credentials from AWS profile" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="AWS Credential Provider should cache credentials" name="AWS Credential Provider should cache credentials" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="AWS Credential Provider should clear credential cache" name="AWS Credential Provider should clear credential cache" time="0">
|
||||
</testcase>
|
||||
<testcase classname="AWS Credential Provider should get Docker environment variables" name="AWS Credential Provider should get Docker environment variables" time="0">
|
||||
</testcase>
|
||||
<testcase classname="AWS Credential Provider should throw error if AWS_PROFILE is not set" name="AWS Credential Provider should throw error if AWS_PROFILE is not set" time="0.006">
|
||||
</testcase>
|
||||
<testcase classname="AWS Credential Provider should throw error for non-existent profile" name="AWS Credential Provider should throw error for non-existent profile" time="0">
|
||||
</testcase>
|
||||
<testcase classname="AWS Credential Provider should throw error for incomplete credentials" name="AWS Credential Provider should throw error for incomplete credentials" time="0.001">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="Container Execution E2E Tests" errors="0" failures="0" skipped="0" timestamp="2025-05-24T18:17:16" time="0.018" tests="3">
|
||||
<testcase classname="Container Execution E2E Tests Container should be properly configured" name="Container Execution E2E Tests Container should be properly configured" time="0.001">
|
||||
</testcase>
|
||||
<testcase classname="Container Execution E2E Tests Should process a simple Claude request" name="Container Execution E2E Tests Should process a simple Claude request" time="0">
|
||||
</testcase>
|
||||
<testcase classname="Container Execution E2E Tests Should handle errors gracefully" name="Container Execution E2E Tests Should handle errors gracefully" time="0">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
<testsuite name="GitHub Controller" errors="0" failures="0" skipped="0" timestamp="2025-05-24T18:17:16" time="0.039" tests="4">
|
||||
<testcase classname="GitHub Controller should process a valid webhook with @TestBot mention" name="GitHub Controller should process a valid webhook with @TestBot mention" time="0.002">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller should reject a webhook with invalid signature" name="GitHub Controller should reject a webhook with invalid signature" time="0.007">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller should ignore comments without @TestBot mention" name="GitHub Controller should ignore comments without @TestBot mention" time="0">
|
||||
</testcase>
|
||||
<testcase classname="GitHub Controller should handle errors from Claude service" name="GitHub Controller should handle errors from Claude service" time="0.004">
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
66
test/MIGRATION_NOTICE.md
Normal file
66
test/MIGRATION_NOTICE.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Test Migration Notice
|
||||
|
||||
## Shell Scripts Migrated to Jest E2E Tests
|
||||
|
||||
The following shell test scripts have been migrated to the Jest E2E test suite and have been removed:
|
||||
|
||||
### Migrated Shell Scripts (✅ Completed)
|
||||
|
||||
**AWS Tests** (Directory: `test/aws/` - removed)
|
||||
|
||||
- `test-aws-mount.sh` → `test/e2e/scenarios/aws-authentication.test.js`
|
||||
- `test-aws-profile.sh` → `test/e2e/scenarios/aws-authentication.test.js`
|
||||
|
||||
**Claude Tests** (Directory: `test/claude/` - removed)
|
||||
|
||||
- `test-claude-direct.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-installation.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-no-firewall.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-response.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
|
||||
**Container Tests** (Directory: `test/container/` - removed)
|
||||
|
||||
- `test-basic-container.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test-container-cleanup.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test-container-privileged.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
|
||||
**Security Tests** (Directory: `test/security/` - removed)
|
||||
|
||||
- `test-firewall.sh` → `test/e2e/scenarios/security-firewall.test.js`
|
||||
- `test-github-token.sh` → `test/e2e/scenarios/github-integration.test.js`
|
||||
- `test-with-auth.sh` → `test/e2e/scenarios/security-firewall.test.js`
|
||||
|
||||
**Integration Tests** (Directory: `test/integration/` - removed)
|
||||
|
||||
- `test-full-flow.sh` → `test/e2e/scenarios/full-workflow.test.js`
|
||||
- `test-claudecode-docker.sh` → `test/e2e/scenarios/docker-execution.test.js` and `full-workflow.test.js`
|
||||
|
||||
### Retained Shell Scripts
|
||||
|
||||
The following scripts contain unique functionality not yet migrated:
|
||||
|
||||
- `test/claude/test-claude.sh` - Contains specific Claude CLI testing logic
|
||||
- `test/container/test-container.sh` - Contains container validation logic
|
||||
|
||||
## Running the New E2E Tests
|
||||
|
||||
To run the migrated E2E tests:
|
||||
|
||||
```bash
|
||||
# Run all E2E tests
|
||||
npm run test:e2e
|
||||
|
||||
# Run specific scenario
|
||||
npx jest test/e2e/scenarios/aws-authentication.test.js
|
||||
```
|
||||
|
||||
## CI/CD Considerations
|
||||
|
||||
The E2E tests require:
|
||||
|
||||
- Docker daemon access
|
||||
- `claude-code-runner:latest` Docker image
|
||||
- Optional: Real GitHub token for full GitHub API tests
|
||||
- Optional: AWS credentials for full AWS tests
|
||||
|
||||
Most tests will run with mock credentials, but some functionality will be skipped.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user