forked from claude-did-this/claude-hub
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
28
.codecov.yml
Normal file
28
.codecov.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
|
||||
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
|
||||
46
.env.example
46
.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,9 +48,13 @@ 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
|
||||
REPO_CACHE_DIR=/tmp/repo-cache
|
||||
REPO_CACHE_MAX_AGE_MS=3600000
|
||||
CONTAINER_LIFETIME_MS=7200000 # Container execution timeout in milliseconds (default: 2 hours)
|
||||
@@ -40,5 +70,19 @@ 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
|
||||
|
||||
# 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
|
||||
248
.github/CLAUDE.md
vendored
Normal file
248
.github/CLAUDE.md
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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
|
||||
|
||||
### 1. CI Workflow (`ci.yml`)
|
||||
- Runs on every PR and push
|
||||
- Tests, linting, security scans
|
||||
- No deployments or publishing
|
||||
|
||||
### 2. Deploy Workflow (`deploy.yml`)
|
||||
- Runs on main branch and tags only
|
||||
- Builds and deploys applications
|
||||
- Includes staging and production environments
|
||||
|
||||
### 3. Security Workflow (`security.yml`)
|
||||
- Runs on schedule and PRs
|
||||
- Comprehensive security scanning
|
||||
- Blocks merging on critical issues
|
||||
|
||||
### 4. Release Workflow (`release.yml`)
|
||||
- Runs on version tags only
|
||||
- Creates GitHub releases
|
||||
- Publishes to package registries
|
||||
|
||||
## 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
|
||||
60
.github/workflows/ci.yml
vendored
60
.github/workflows/ci.yml
vendored
@@ -2,9 +2,7 @@ name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
@@ -91,32 +89,6 @@ jobs:
|
||||
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:
|
||||
@@ -125,8 +97,16 @@ jobs:
|
||||
needs: [test-unit]
|
||||
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
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 code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -139,20 +119,24 @@ jobs:
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Generate test coverage
|
||||
run: npm run test:coverage
|
||||
run: npm run test:ci
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
- name: Fix coverage file permissions
|
||||
run: |
|
||||
# Fix permissions on coverage files that may be created with restricted access
|
||||
find coverage -type f -exec chmod 644 {} \; 2>/dev/null || true
|
||||
find coverage -type d -exec chmod 755 {} \; 2>/dev/null || true
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: intelligence-assist/claude-hub
|
||||
|
||||
# Security scans - run on GitHub for faster execution
|
||||
security:
|
||||
@@ -209,9 +193,9 @@ jobs:
|
||||
# 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')
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on main branch or version tags, not on PRs
|
||||
if: (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 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]
|
||||
|
||||
|
||||
29
.github/workflows/deploy.yml
vendored
29
.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,7 +149,7 @@ 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'
|
||||
|
||||
@@ -164,8 +161,10 @@ jobs:
|
||||
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/', '')}`
|
||||
});
|
||||
|
||||
|
||||
147
.github/workflows/docker-publish.yml
vendored
147
.github/workflows/docker-publish.yml
vendored
@@ -7,42 +7,45 @@ 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' }}
|
||||
# Runner configuration - set USE_SELF_HOSTED to 'false' to force GitHub-hosted runners
|
||||
USE_SELF_HOSTED: ${{ vars.USE_SELF_HOSTED || 'true' }}
|
||||
|
||||
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'
|
||||
# Use self-hosted runners by default, with ability to override via repository variable
|
||||
runs-on: ${{ vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || fromJSON('["self-hosted", "linux", "x64", "docker"]') }}
|
||||
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 +63,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 +116,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: ${{ vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || fromJSON('["self-hosted", "linux", "x64", "docker"]') }}
|
||||
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 +152,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 +167,28 @@ 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
|
||||
|
||||
# Fallback job if self-hosted runners timeout
|
||||
build-fallback:
|
||||
needs: [build, build-claudecode]
|
||||
if: |
|
||||
always() &&
|
||||
(needs.build.result == 'failure' || needs.build-claudecode.result == 'failure') &&
|
||||
vars.USE_SELF_HOSTED != 'false'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Trigger rebuild on GitHub-hosted runners
|
||||
run: |
|
||||
echo "Self-hosted runner build failed. To retry with GitHub-hosted runners:"
|
||||
echo "1. Set the repository variable USE_SELF_HOSTED to 'false'"
|
||||
echo "2. Re-run this workflow"
|
||||
echo ""
|
||||
echo "Or manually trigger a new workflow run with GitHub-hosted runners."
|
||||
exit 1
|
||||
360
.github/workflows/pr.yml
vendored
Normal file
360
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,360 @@
|
||||
name: Pull Request CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
|
||||
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
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.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'
|
||||
|
||||
# Coverage generation for PR feedback
|
||||
coverage:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-unit]
|
||||
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
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 code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- 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:ci
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
- name: Fix coverage file permissions
|
||||
run: |
|
||||
# Fix permissions on coverage files that may be created with restricted access
|
||||
find coverage -type f -exec chmod 644 {} \; 2>/dev/null || true
|
||||
find coverage -type d -exec chmod 755 {} \; 2>/dev/null || true
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: intelligence-assist/claude-hub
|
||||
|
||||
# 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'
|
||||
|
||||
# Security scans for PRs
|
||||
security:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history for secret scanning
|
||||
|
||||
- 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 || {
|
||||
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 || echo "::warning::Security audit script failed"
|
||||
|
||||
- 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.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
extra_args: --debug --only-verified
|
||||
|
||||
- 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 for PRs
|
||||
codeql:
|
||||
name: CodeQL Analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript
|
||||
config-file: ./.github/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:javascript"
|
||||
|
||||
# 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 build test for PRs (build only, don't push)
|
||||
docker-build:
|
||||
name: Docker Build Test
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.changes.outputs.docker == 'true' || needs.changes.outputs.src == 'true'
|
||||
needs: [test-unit, lint, changes, security, codeql]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build main Docker image (test only)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
tags: claude-github-webhook:pr-test
|
||||
cache-from: type=gha,scope=pr-main
|
||||
cache-to: type=gha,mode=max,scope=pr-main
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build Claude Code Docker image (test only)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.claudecode
|
||||
push: false
|
||||
load: true
|
||||
tags: claude-code-runner:pr-test
|
||||
cache-from: type=gha,scope=pr-claudecode
|
||||
cache-to: type=gha,mode=max,scope=pr-claudecode
|
||||
platforms: linux/amd64
|
||||
|
||||
- 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:pr-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
|
||||
|
||||
- name: Docker security scan
|
||||
if: needs.changes.outputs.docker == 'true'
|
||||
run: |
|
||||
# Run Hadolint on Dockerfile
|
||||
docker run --rm -i hadolint/hadolint < Dockerfile || echo "::warning::Dockerfile linting issues found"
|
||||
|
||||
# Run Trivy scan on built image
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v $HOME/Library/Caches:/root/.cache/ \
|
||||
aquasec/trivy:latest image --exit-code 0 --severity HIGH,CRITICAL \
|
||||
claude-github-webhook:pr-test || echo "::warning::Security vulnerabilities found"
|
||||
|
||||
# Summary job that all others depend on
|
||||
pr-summary:
|
||||
name: PR Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test-unit, coverage, test-integration, security, codeql, docker-build]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Check job statuses
|
||||
run: |
|
||||
echo "## Pull Request CI Summary"
|
||||
echo "- Lint & Format: ${{ needs.lint.result }}"
|
||||
echo "- Unit Tests: ${{ needs.test-unit.result }}"
|
||||
echo "- Test Coverage: ${{ needs.coverage.result }}"
|
||||
echo "- Integration Tests: ${{ needs.test-integration.result }}"
|
||||
echo "- Security Scan: ${{ needs.security.result }}"
|
||||
echo "- CodeQL Analysis: ${{ needs.codeql.result }}"
|
||||
echo "- Docker Build: ${{ needs.docker-build.result }}"
|
||||
|
||||
# Check for any failures
|
||||
if [[ "${{ needs.lint.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.test-unit.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.coverage.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.test-integration.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.security.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.codeql.result }}" == "failure" ]] || \
|
||||
[[ "${{ needs.docker-build.result }}" == "failure" ]]; then
|
||||
echo "::error::One or more CI jobs failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All CI checks passed!"
|
||||
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
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -22,6 +22,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 +77,12 @@ config
|
||||
auth.json
|
||||
service-account.json
|
||||
|
||||
# Claude authentication output
|
||||
.claude-hub/
|
||||
|
||||
# Docker secrets
|
||||
secrets/
|
||||
|
||||
# Benchmark results
|
||||
benchmark_results_*.json
|
||||
|
||||
# Temporary and backup files
|
||||
*.backup
|
||||
|
||||
140
CLAUDE.md
140
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`
|
||||
|
||||
@@ -82,16 +89,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 +152,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 +207,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 +234,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"]
|
||||
90
Dockerfile.claude-setup
Normal file
90
Dockerfile.claude-setup
Normal file
@@ -0,0 +1,90 @@
|
||||
FROM node:24
|
||||
|
||||
# Install dependencies for interactive session
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
sudo \
|
||||
zsh \
|
||||
curl \
|
||||
vim \
|
||||
nano \
|
||||
gh
|
||||
|
||||
# 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 that captures authentication state
|
||||
RUN cat > /setup-claude-auth.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔧 Claude Authentication Setup Container"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "This container allows you to authenticate with Claude interactively"
|
||||
echo "and capture the authentication state for use in other containers."
|
||||
echo ""
|
||||
echo "Instructions:"
|
||||
echo "1. Run: claude login"
|
||||
echo "2. Follow the authentication flow"
|
||||
echo "3. Test with: claude status"
|
||||
echo "4. Type 'exit' when authentication is working"
|
||||
echo ""
|
||||
echo "The ~/.claude directory will be preserved in /auth-output"
|
||||
echo ""
|
||||
|
||||
# Function to copy authentication state
|
||||
copy_auth_state() {
|
||||
if [ -d "/home/node/.claude" ] && [ -d "/auth-output" ]; then
|
||||
echo "💾 Copying authentication state..."
|
||||
cp -r /home/node/.claude/* /auth-output/ 2>/dev/null || true
|
||||
cp -r /home/node/.claude/.* /auth-output/ 2>/dev/null || true
|
||||
chown -R node:node /auth-output
|
||||
echo "✅ Authentication state copied to /auth-output"
|
||||
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 "💡 Tip: Run 'claude --version' to verify Claude CLI is available"
|
||||
echo ""
|
||||
|
||||
# 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 "Environment ready! Claude CLI is available at: $(which claude || echo "/usr/local/share/npm-global/bin/claude")"
|
||||
echo "Run: claude login"
|
||||
exec bash -i
|
||||
'
|
||||
EOF
|
||||
|
||||
RUN chmod +x /setup-claude-auth.sh
|
||||
|
||||
# Set entrypoint to setup script
|
||||
ENTRYPOINT ["/setup-claude-auth.sh"]
|
||||
@@ -72,8 +72,14 @@ 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 entrypoint scripts
|
||||
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
|
||||
COPY scripts/runtime/claudecode-tagging-entrypoint.sh /scripts/runtime/claudecode-tagging-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh && \
|
||||
chmod +x /scripts/runtime/claudecode-entrypoint.sh && \
|
||||
chmod +x /scripts/runtime/claudecode-tagging-entrypoint.sh
|
||||
|
||||
# Set the default shell to bash
|
||||
ENV SHELL /bin/zsh
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
746
README.md
746
README.md
@@ -3,399 +3,399 @@
|
||||
[](https://github.com/intelligence-assist/claude-hub/actions/workflows/ci.yml)
|
||||
[](https://github.com/intelligence-assist/claude-hub/actions/workflows/security.yml)
|
||||
[](test/README.md)
|
||||
[](./coverage/index.html)
|
||||
[](https://codecov.io/gh/intelligence-assist/claude-hub)
|
||||
[](https://github.com/intelligence-assist/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.
|
||||

|
||||
|
||||
## ⚡ Performance Optimizations
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Docker Image (Recommended)
|
||||
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull intelligenceassist/claude-hub:latest
|
||||
|
||||
# Run with environment variables
|
||||
docker run -d \
|
||||
--name claude-webhook \
|
||||
-p 8082:3002 \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e GITHUB_TOKEN=your_github_token \
|
||||
-e GITHUB_WEBHOOK_SECRET=your_webhook_secret \
|
||||
-e ANTHROPIC_API_KEY=your_anthropic_key \
|
||||
-e BOT_USERNAME=@YourBotName \
|
||||
-e AUTHORIZED_USERS=user1,user2 \
|
||||
intelligenceassist/claude-hub:latest
|
||||
|
||||
# Or use Docker Compose
|
||||
wget https://raw.githubusercontent.com/intelligence-assist/claude-hub/main/docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Option 2: From Source
|
||||
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone https://github.com/intelligence-assist/claude-hub.git
|
||||
cd claude-hub
|
||||
./scripts/setup/setup-secure-credentials.sh
|
||||
|
||||
# Launch with Docker Compose
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Service runs on `http://localhost:8082` by default.
|
||||
|
||||
## 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 login # Follow browser 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:8082/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:8082/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/intelligence-assist/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'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
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,19 +2,17 @@ services:
|
||||
webhook:
|
||||
build: .
|
||||
ports:
|
||||
- "8082:3002"
|
||||
- "8082:3003"
|
||||
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=3003
|
||||
- TRUST_PROXY=${TRUST_PROXY:-true}
|
||||
- AUTHORIZED_USERS=${AUTHORIZED_USERS:-Cheffromspace}
|
||||
- BOT_USERNAME=${BOT_USERNAME:-@MCPClaude}
|
||||
- DEFAULT_GITHUB_OWNER=${DEFAULT_GITHUB_OWNER:-Cheffromspace}
|
||||
@@ -22,28 +20,22 @@ services:
|
||||
- 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
|
||||
# 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:3003/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
|
||||
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,4 +1,6 @@
|
||||
const js = require('@eslint/js');
|
||||
const tseslint = require('@typescript-eslint/eslint-plugin');
|
||||
const tsparser = require('@typescript-eslint/parser');
|
||||
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
@@ -65,9 +67,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 +126,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,33 @@
|
||||
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',
|
||||
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' }]
|
||||
],
|
||||
]
|
||||
};
|
||||
3042
package-lock.json
generated
3042
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -1,18 +1,27 @@
|
||||
{
|
||||
"name": "claude-github-webhook",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"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/",
|
||||
@@ -23,17 +32,29 @@
|
||||
"setup:dev": "husky install"
|
||||
},
|
||||
"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",
|
||||
"husky": "^9.1.7",
|
||||
@@ -41,6 +62,11 @@
|
||||
"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!"
|
||||
@@ -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!"
|
||||
@@ -13,6 +13,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}"
|
||||
@@ -45,8 +81,26 @@ 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"
|
||||
@@ -65,9 +119,18 @@ 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}" \
|
||||
|
||||
135
scripts/runtime/claudecode-tagging-entrypoint.sh
Executable file
135
scripts/runtime/claudecode-tagging-entrypoint.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Minimal entrypoint for auto-tagging workflow
|
||||
# Only allows Read and GitHub tools for security
|
||||
|
||||
# Environment variables (passed from service)
|
||||
# Simply reference the variables directly - no need to reassign
|
||||
# They are already available in the environment
|
||||
|
||||
# Ensure workspace directory exists and has proper permissions
|
||||
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}"
|
||||
echo "${GITHUB_TOKEN}" | sudo -u node gh auth login --with-token
|
||||
sudo -u node gh auth setup-git
|
||||
else
|
||||
echo "No GitHub token provided, skipping GitHub authentication"
|
||||
fi
|
||||
|
||||
# Clone the repository as node user (needed for context)
|
||||
if [ -n "${GITHUB_TOKEN}" ] && [ -n "${REPO_FULL_NAME}" ]; then
|
||||
echo "Cloning repository ${REPO_FULL_NAME}..." >&2
|
||||
sudo -u node git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO_FULL_NAME}.git" /workspace/repo >&2
|
||||
cd /workspace/repo
|
||||
else
|
||||
echo "Skipping repository clone - missing GitHub token or repository name" >&2
|
||||
cd /workspace
|
||||
fi
|
||||
|
||||
# Checkout main branch (tagging doesn't need specific branches)
|
||||
echo "Using main branch" >&2
|
||||
sudo -u node git checkout main >&2 || sudo -u node git checkout master >&2
|
||||
|
||||
# Configure git for minimal operations
|
||||
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 Claude authentication
|
||||
# Support both API key and interactive auth methods
|
||||
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"
|
||||
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 minimal tools for auto-tagging
|
||||
echo "Running Claude Code for auto-tagging..." >&2
|
||||
|
||||
# Check if command exists
|
||||
if [ -z "${COMMAND}" ]; then
|
||||
echo "ERROR: No command provided. COMMAND environment variable is empty." | tee -a "${RESPONSE_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Log the command length for debugging
|
||||
echo "Command length: ${#COMMAND}" >&2
|
||||
|
||||
# Run Claude Code with minimal tool set: Read (for repository context) and GitHub (for label operations)
|
||||
# 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="$CLAUDE_USER_HOME" \
|
||||
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
|
||||
GH_TOKEN="${GITHUB_TOKEN}" \
|
||||
/usr/local/share/npm-global/bin/claude \
|
||||
--allowedTools Read,GitHub \
|
||||
--print "${COMMAND}" \
|
||||
> "${RESPONSE_FILE}" 2>&1
|
||||
|
||||
# Check for errors
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Claude Code execution failed. See logs for details." | tee -a "${RESPONSE_FILE}" >&2
|
||||
fi
|
||||
|
||||
# Output the response
|
||||
cat "${RESPONSE_FILE}"
|
||||
@@ -13,4 +13,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
|
||||
@@ -10,6 +10,16 @@ else
|
||||
echo "Warning: Failed to build Claude Code runner image. Service will attempt to build on first use."
|
||||
fi
|
||||
|
||||
# Ensure dependencies are installed (in case volume mount affected node_modules)
|
||||
if [ ! -d "node_modules" ] || [ ! -f "node_modules/.bin/tsc" ]; then
|
||||
echo "Installing dependencies..."
|
||||
npm ci
|
||||
fi
|
||||
|
||||
# Always compile TypeScript to ensure we have the latest compiled source
|
||||
echo "Compiling TypeScript..."
|
||||
npm run build
|
||||
|
||||
# Start the webhook service
|
||||
echo "Starting webhook service..."
|
||||
exec node src/index.js
|
||||
exec node dist/index.js
|
||||
@@ -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
|
||||
66
scripts/setup/setup-claude-interactive.sh
Executable file
66
scripts/setup/setup-claude-interactive.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/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 interactive Claude authentication container..."
|
||||
echo ""
|
||||
echo "IMPORTANT: This will open an interactive shell where you can:"
|
||||
echo " 1. Run 'claude --dangerously-skip-permissions' to authenticate"
|
||||
echo " 2. Follow the authentication flow"
|
||||
echo " 3. Type 'exit' when done to preserve authentication state"
|
||||
echo ""
|
||||
echo "The authenticated ~/.claude directory will be saved to:"
|
||||
echo " $AUTH_OUTPUT_DIR"
|
||||
echo ""
|
||||
read -p "Press Enter to continue or Ctrl+C to cancel..."
|
||||
|
||||
# Run the interactive container
|
||||
docker run -it --rm \
|
||||
-v "$AUTH_OUTPUT_DIR:/auth-output" \
|
||||
-v "$HOME/.gitconfig:/home/node/.gitconfig:ro" \
|
||||
--name claude-auth-setup \
|
||||
claude-setup:latest
|
||||
|
||||
echo ""
|
||||
echo "📋 Checking authentication output..."
|
||||
|
||||
if [ -f "$AUTH_OUTPUT_DIR/.credentials.json" ] || [ -f "$AUTH_OUTPUT_DIR/settings.local.json" ]; then
|
||||
echo "✅ Authentication files found in $AUTH_OUTPUT_DIR"
|
||||
echo ""
|
||||
echo "📁 Captured authentication files:"
|
||||
find "$AUTH_OUTPUT_DIR" -type f -name "*.json" -o -name "*.db" | head -10
|
||||
echo ""
|
||||
echo "🔄 To use this authentication in your webhook service:"
|
||||
echo " 1. Copy files to your ~/.claude directory:"
|
||||
echo " cp -r $AUTH_OUTPUT_DIR/* ~/.claude/"
|
||||
echo " 2. Or update docker-compose.yml to mount the auth directory:"
|
||||
echo " - $AUTH_OUTPUT_DIR:/home/node/.claude:ro"
|
||||
echo ""
|
||||
else
|
||||
echo "⚠️ No authentication files found. You may need to:"
|
||||
echo " 1. Run the container again and complete the authentication flow"
|
||||
echo " 2. Ensure you ran 'claude --dangerously-skip-permissions' and completed authentication"
|
||||
echo " 3. Check that you have an active Claude Code subscription"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🧪 Testing authentication..."
|
||||
echo "You can test the captured authentication with:"
|
||||
echo " docker run --rm -v \"$AUTH_OUTPUT_DIR:/home/node/.claude:ro\" claude-setup:latest claude --dangerously-skip-permissions --print 'test'"
|
||||
@@ -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
1476
src/controllers/githubController.ts
Normal file
1476
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)`);
|
||||
});
|
||||
193
src/index.ts
Normal file
193
src/index.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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'] ?? '3003', 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
|
||||
const generalRateLimit = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
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.'
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false // Disable the `X-RateLimit-*` headers
|
||||
});
|
||||
|
||||
const webhookRateLimit = rateLimit({
|
||||
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.'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
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();
|
||||
try {
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
// 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
|
||||
};
|
||||
711
src/services/claudeService.ts
Normal file
711
src/services/claudeService.ts
Normal file
@@ -0,0 +1,711 @@
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Select appropriate entrypoint script based on operation type
|
||||
const entrypointScript = getEntrypointScript(operationType);
|
||||
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 appropriate entrypoint script based on operation type
|
||||
*/
|
||||
function getEntrypointScript(operationType: OperationType): string {
|
||||
switch (operationType) {
|
||||
case 'auto-tagging':
|
||||
return '/scripts/runtime/claudecode-tagging-entrypoint.sh';
|
||||
case 'pr-review':
|
||||
case 'default':
|
||||
default:
|
||||
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 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.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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') ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
136
src/types/claude.ts
Normal file
136
src/types/claude.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
export type OperationType = 'auto-tagging' | '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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
151
src/types/express.ts
Normal file
151
src/types/express.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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;
|
||||
}
|
||||
211
src/types/github.ts
Normal file
211
src/types/github.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
64
test/MIGRATION_NOTICE.md
Normal file
64
test/MIGRATION_NOTICE.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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 can be safely removed:
|
||||
|
||||
### AWS Tests
|
||||
|
||||
- `test/aws/test-aws-mount.sh` → Replaced by `test/e2e/scenarios/aws-authentication.test.js`
|
||||
- `test/aws/test-aws-profile.sh` → Replaced by `test/e2e/scenarios/aws-authentication.test.js`
|
||||
|
||||
### Claude Tests
|
||||
|
||||
- `test/claude/test-claude-direct.sh` → Replaced by `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test/claude/test-claude-installation.sh` → Replaced by `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test/claude/test-claude-no-firewall.sh` → Replaced by `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test/claude/test-claude-response.sh` → Replaced by `test/e2e/scenarios/claude-integration.test.js`
|
||||
|
||||
### Container Tests
|
||||
|
||||
- `test/container/test-basic-container.sh` → Replaced by `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test/container/test-container-cleanup.sh` → Replaced by `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test/container/test-container-privileged.sh` → Replaced by `test/e2e/scenarios/container-execution.test.js`
|
||||
|
||||
### Security Tests
|
||||
|
||||
- `test/security/test-firewall.sh` → Replaced by `test/e2e/scenarios/security-firewall.test.js`
|
||||
- `test/security/test-github-token.sh` → Replaced by `test/e2e/scenarios/github-integration.test.js`
|
||||
- `test/security/test-with-auth.sh` → Replaced by `test/e2e/scenarios/security-firewall.test.js`
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `test/integration/test-full-flow.sh` → Replaced by `test/e2e/scenarios/full-workflow.test.js`
|
||||
- `test/integration/test-claudecode-docker.sh` → Replaced by `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.
|
||||
@@ -9,6 +9,7 @@ This directory contains the test framework for the Claude Webhook service. The t
|
||||
/unit # Unit tests for individual components
|
||||
/controllers # Tests for controllers
|
||||
/services # Tests for services
|
||||
/security # Security-focused tests
|
||||
/utils # Tests for utility functions
|
||||
/integration # Integration tests between components
|
||||
/github # GitHub integration tests
|
||||
@@ -52,14 +53,25 @@ npm run test:watch
|
||||
|
||||
Unit tests focus on testing individual components in isolation. They use Jest's mocking capabilities to replace dependencies with test doubles. These tests are fast and reliable, making them ideal for development and CI/CD pipelines.
|
||||
|
||||
#### Chatbot Provider Tests
|
||||
|
||||
The chatbot provider system includes comprehensive unit tests for:
|
||||
|
||||
- **Base Provider Interface** (`ChatbotProvider.test.js`): Tests the abstract base class and inheritance patterns
|
||||
- **Discord Provider** (`DiscordProvider.test.js`): Tests Discord-specific webhook handling, signature verification, and message parsing
|
||||
- **Provider Factory** (`ProviderFactory.test.js`): Tests dependency injection and provider management
|
||||
- **Security Tests** (`signature-verification.test.js`): Tests webhook signature verification and security edge cases
|
||||
- **Payload Tests** (`discord-payloads.test.js`): Tests real Discord webhook payloads and edge cases
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
// Test for awsCredentialProvider.js
|
||||
describe('AWS Credential Provider', () => {
|
||||
test('should get credentials from AWS profile', async () => {
|
||||
const credentials = await awsCredentialProvider.getCredentials();
|
||||
expect(credentials).toBeDefined();
|
||||
// Test for DiscordProvider.js
|
||||
describe('Discord Provider', () => {
|
||||
test('should parse Discord slash command correctly', () => {
|
||||
const payload = { type: 2, data: { name: 'claude' } };
|
||||
const result = provider.parseWebhookPayload(payload);
|
||||
expect(result.type).toBe('command');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing AWS mount and profile..."
|
||||
|
||||
docker run --rm \
|
||||
-v $HOME/.aws:/home/node/.aws:ro \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "echo '=== AWS files ==='; ls -la /home/node/.aws/; echo '=== Config content ==='; cat /home/node/.aws/config; echo '=== Test AWS profile ==='; export AWS_PROFILE=claude-webhook; export AWS_CONFIG_FILE=/home/node/.aws/config; export AWS_SHARED_CREDENTIALS_FILE=/home/node/.aws/credentials; aws sts get-caller-identity --profile claude-webhook"
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script to verify AWS profile authentication is working
|
||||
|
||||
echo "AWS Profile Authentication Test"
|
||||
echo "==============================="
|
||||
echo
|
||||
|
||||
# Source .env file if it exists
|
||||
if [ -f ../.env ]; then
|
||||
export $(cat ../.env | grep -v '^#' | xargs)
|
||||
echo "Loaded configuration from .env"
|
||||
else
|
||||
echo "No .env file found"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Current configuration:"
|
||||
echo "USE_AWS_PROFILE: ${USE_AWS_PROFILE:-not set}"
|
||||
echo "AWS_PROFILE: ${AWS_PROFILE:-not set}"
|
||||
echo "AWS_REGION: ${AWS_REGION:-not set}"
|
||||
echo
|
||||
|
||||
# Test if profile exists
|
||||
if [ "$USE_AWS_PROFILE" = "true" ] && [ -n "$AWS_PROFILE" ]; then
|
||||
echo "Testing AWS profile: $AWS_PROFILE"
|
||||
|
||||
# Check if profile exists in credentials file
|
||||
if aws configure list --profile "$AWS_PROFILE" >/dev/null 2>&1; then
|
||||
echo "✅ Profile exists in AWS credentials"
|
||||
|
||||
# Test authentication
|
||||
echo
|
||||
echo "Testing authentication..."
|
||||
if aws sts get-caller-identity --profile "$AWS_PROFILE" >/dev/null 2>&1; then
|
||||
echo "✅ Authentication successful!"
|
||||
echo
|
||||
echo "Account details:"
|
||||
aws sts get-caller-identity --profile "$AWS_PROFILE" --output table
|
||||
|
||||
# Test Claude service access
|
||||
echo
|
||||
echo "Testing access to Claude service (Bedrock)..."
|
||||
if aws bedrock list-foundation-models --profile "$AWS_PROFILE" --region "$AWS_REGION" >/dev/null 2>&1; then
|
||||
echo "✅ Can access Bedrock service"
|
||||
|
||||
# Check for Claude models
|
||||
echo "Available Claude models:"
|
||||
aws bedrock list-foundation-models --profile "$AWS_PROFILE" --region "$AWS_REGION" \
|
||||
--query "modelSummaries[?contains(modelId, 'claude')].{ID:modelId,Name:modelName}" \
|
||||
--output table
|
||||
else
|
||||
echo "❌ Cannot access Bedrock service. Check permissions."
|
||||
fi
|
||||
else
|
||||
echo "❌ Authentication failed. Check your credentials."
|
||||
fi
|
||||
else
|
||||
echo "❌ Profile '$AWS_PROFILE' not found in AWS credentials"
|
||||
echo
|
||||
echo "Available profiles:"
|
||||
aws configure list-profiles
|
||||
fi
|
||||
else
|
||||
echo "AWS profile usage is not enabled or profile not set."
|
||||
echo "Using environment variables for authentication."
|
||||
|
||||
# Test with environment variables
|
||||
if [ -n "$AWS_ACCESS_KEY_ID" ]; then
|
||||
echo
|
||||
echo "Testing with environment variables..."
|
||||
if aws sts get-caller-identity >/dev/null 2>&1; then
|
||||
echo "✅ Authentication successful with environment variables"
|
||||
else
|
||||
echo "❌ Authentication failed with environment variables"
|
||||
fi
|
||||
else
|
||||
echo "No AWS credentials found in environment variables either."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Test complete!"
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing Claude Code directly in container..."
|
||||
|
||||
docker run --rm \
|
||||
-v $HOME/.aws:/home/node/.aws:ro \
|
||||
-e AWS_PROFILE="claude-webhook" \
|
||||
-e AWS_REGION="us-east-2" \
|
||||
-e CLAUDE_CODE_USE_BEDROCK="1" \
|
||||
-e ANTHROPIC_MODEL="us.anthropic.claude-3-7-sonnet-20250219-v1:0" \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "cd /workspace && export PATH=/usr/local/share/npm-global/bin:$PATH && sudo -u node -E env PATH=/usr/local/share/npm-global/bin:$PATH AWS_PROFILE=claude-webhook AWS_REGION=us-east-2 CLAUDE_CODE_USE_BEDROCK=1 ANTHROPIC_MODEL=us.anthropic.claude-3-7-sonnet-20250219-v1:0 AWS_CONFIG_FILE=/home/node/.aws/config AWS_SHARED_CREDENTIALS_FILE=/home/node/.aws/credentials claude --print 'Hello world' 2>&1"
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Checking Claude installation..."
|
||||
|
||||
docker run --rm \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "echo '=== As root ==='; which claude; claude --version 2>&1 || echo 'Error: $?'; echo '=== As node user ==='; sudo -u node which claude; sudo -u node claude --version 2>&1 || echo 'Error: $?'; echo '=== Check PATH ==='; echo \$PATH; echo '=== Check npm global ==='; ls -la /usr/local/share/npm-global/bin/; echo '=== Check node user config ==='; ls -la /home/node/.claude/"
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing Claude without firewall..."
|
||||
|
||||
docker run --rm \
|
||||
-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 && claude --print 'Hello world' 2>&1"
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing Claude response directly..."
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_RAW \
|
||||
--cap-add=SYS_TIME \
|
||||
--cap-add=DAC_OVERRIDE \
|
||||
--cap-add=AUDIT_WRITE \
|
||||
--cap-add=SYS_ADMIN \
|
||||
-v $HOME/.aws:/home/node/.aws:ro \
|
||||
-e REPO_FULL_NAME="${TEST_REPO_FULL_NAME:-owner/repo}" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="What is this repository?" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-dummy-token}" \
|
||||
-e AWS_PROFILE="claude-webhook" \
|
||||
-e AWS_REGION="us-east-2" \
|
||||
-e CLAUDE_CODE_USE_BEDROCK="1" \
|
||||
-e ANTHROPIC_MODEL="us.anthropic.claude-3-7-sonnet-20250219-v1:0" \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "/usr/local/bin/entrypoint.sh; echo '=== Response file content ==='; cat /workspace/response.txt; echo '=== Exit code ==='; echo \$?"
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/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!"
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing basic container functionality..."
|
||||
|
||||
# Test without any special environment vars to bypass entrypoint
|
||||
docker run --rm \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "echo 'Container works' && ls -la /home/node/"
|
||||
|
||||
echo "Testing AWS credentials volume mount..."
|
||||
docker run --rm \
|
||||
-v $HOME/.aws:/home/node/.aws:ro \
|
||||
--entrypoint /bin/bash \
|
||||
claude-code-runner:latest \
|
||||
-c "ls -la /home/node/.aws/"
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Clean up a test container for E2E tests
|
||||
|
||||
CONTAINER_ID="$1"
|
||||
|
||||
if [ -z "$CONTAINER_ID" ]; then
|
||||
echo "Error: No container ID provided"
|
||||
echo "Usage: $0 <container-id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Stopping container $CONTAINER_ID..."
|
||||
docker stop "$CONTAINER_ID" 2>/dev/null || true
|
||||
|
||||
echo "Removing container $CONTAINER_ID..."
|
||||
docker rm "$CONTAINER_ID" 2>/dev/null || true
|
||||
|
||||
echo "Container cleanup complete."
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Testing container privileges..."
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_RAW \
|
||||
--cap-add=SYS_TIME \
|
||||
--cap-add=DAC_OVERRIDE \
|
||||
--cap-add=AUDIT_WRITE \
|
||||
--cap-add=SYS_ADMIN \
|
||||
-v $HOME/.aws:/home/node/.aws:ro \
|
||||
-e REPO_FULL_NAME="${TEST_REPO_FULL_NAME:-owner/repo}" \
|
||||
-e ISSUE_NUMBER="1" \
|
||||
-e IS_PULL_REQUEST="false" \
|
||||
-e COMMAND="echo test" \
|
||||
-e GITHUB_TOKEN="${GITHUB_TOKEN:-dummy-token}" \
|
||||
-e AWS_PROFILE="claude-webhook" \
|
||||
-e AWS_REGION="us-east-2" \
|
||||
-e CLAUDE_CODE_USE_BEDROCK="1" \
|
||||
-e ANTHROPIC_MODEL="us.anthropic.claude-3-7-sonnet-20250219-v1:0" \
|
||||
claude-code-runner:latest
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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!"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user