forked from claude-did-this/claude-hub
Compare commits
20 Commits
v0.1.1
...
chore/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58734fa62c | ||
|
|
9e5b3c3d20 | ||
|
|
bf1c42f5ca | ||
|
|
f765e2ac3e | ||
|
|
14785b2e64 | ||
|
|
faa60f4f55 | ||
|
|
4ece2969b3 | ||
|
|
295c182351 | ||
|
|
af851491e8 | ||
|
|
31efbbc2bb | ||
|
|
2e5fa7aa26 | ||
|
|
caad85d7a0 | ||
|
|
acf44b1c63 | ||
|
|
e463f2e5c5 | ||
|
|
150626b171 | ||
|
|
b028502a82 | ||
|
|
12e4589169 | ||
|
|
53d77c2856 | ||
|
|
df756e15ae | ||
|
|
f7399f8ad1 |
@@ -1,5 +1,6 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
coverage:
|
||||
status:
|
||||
@@ -25,4 +26,4 @@ comment:
|
||||
|
||||
github_checks:
|
||||
# Disable check suites to prevent hanging on non-main branches
|
||||
annotations: false
|
||||
annotations: false
|
||||
|
||||
28
.env.example
28
.env.example
@@ -55,10 +55,20 @@ CLAUDE_HUB_DIR=/home/user/.claude-hub
|
||||
# Container Settings
|
||||
CLAUDE_USE_CONTAINERS=1
|
||||
CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
CLAUDE_CONTAINER_PRIVILEGED=false
|
||||
REPO_CACHE_DIR=/tmp/repo-cache
|
||||
REPO_CACHE_MAX_AGE_MS=3600000
|
||||
CONTAINER_LIFETIME_MS=7200000 # Container execution timeout in milliseconds (default: 2 hours)
|
||||
|
||||
# Claude Code Timeout Settings (for unattended mode)
|
||||
BASH_DEFAULT_TIMEOUT_MS=600000 # Default timeout for bash commands (10 minutes)
|
||||
BASH_MAX_TIMEOUT_MS=1200000 # Maximum timeout Claude can set (20 minutes)
|
||||
|
||||
# Container Resource Limits
|
||||
CLAUDE_CONTAINER_CPU_SHARES=1024
|
||||
CLAUDE_CONTAINER_MEMORY_LIMIT=2g
|
||||
CLAUDE_CONTAINER_PIDS_LIMIT=256
|
||||
|
||||
# AWS Bedrock Credentials for Claude (if using Bedrock)
|
||||
AWS_ACCESS_KEY_ID=your_aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
|
||||
@@ -76,6 +86,7 @@ CLAUDE_CONTAINER_CAP_NET_RAW=true
|
||||
CLAUDE_CONTAINER_CAP_SYS_TIME=false
|
||||
CLAUDE_CONTAINER_CAP_DAC_OVERRIDE=true
|
||||
CLAUDE_CONTAINER_CAP_AUDIT_WRITE=true
|
||||
CLAUDE_CONTAINER_CAP_SYS_ADMIN=false
|
||||
|
||||
# PR Review Configuration
|
||||
PR_REVIEW_WAIT_FOR_ALL_CHECKS=true
|
||||
@@ -85,4 +96,19 @@ PR_REVIEW_MAX_WAIT_MS=1800000
|
||||
PR_REVIEW_CONDITIONAL_TIMEOUT_MS=300000
|
||||
|
||||
# Test Configuration
|
||||
TEST_REPO_FULL_NAME=owner/repo
|
||||
TEST_REPO_FULL_NAME=owner/repo
|
||||
|
||||
# Security Configuration (optional)
|
||||
# DISABLE_LOG_REDACTION=false # WARNING: Only enable for debugging, exposes sensitive data in logs
|
||||
|
||||
# File-based Secrets (optional, takes priority over environment variables)
|
||||
# GITHUB_TOKEN_FILE=/run/secrets/github_token
|
||||
# ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
|
||||
# GITHUB_WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
|
||||
|
||||
# Authentication Methods (optional)
|
||||
# CLAUDE_AUTH_HOST_DIR=/path/to/claude/auth # For setup container authentication
|
||||
|
||||
# CLI Configuration (optional)
|
||||
# API_URL=http://localhost:3003 # Default API URL for CLI tool
|
||||
# WEBHOOK_URL=http://localhost:3002/api/webhooks/github # Webhook endpoint URL
|
||||
|
||||
25
.env.quickstart
Normal file
25
.env.quickstart
Normal file
@@ -0,0 +1,25 @@
|
||||
# Claude GitHub Webhook - Quick Start Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
#
|
||||
# cp .env.quickstart .env
|
||||
#
|
||||
# Only the essentials to get up and running in 10 minutes
|
||||
|
||||
# GitHub Configuration (Required)
|
||||
GITHUB_TOKEN=ghp_your_github_token_here
|
||||
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
|
||||
# Bot Identity (Required)
|
||||
BOT_USERNAME=@YourBotName
|
||||
BOT_EMAIL=bot@example.com
|
||||
|
||||
# Security - Who can use the bot
|
||||
AUTHORIZED_USERS=your-github-username
|
||||
DEFAULT_AUTHORIZED_USER=your-github-username
|
||||
|
||||
# Port (default: 3002)
|
||||
PORT=3002
|
||||
|
||||
# That's it! The setup script will handle Claude authentication.
|
||||
# Run: ./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
28
.github/CLAUDE.md
vendored
28
.github/CLAUDE.md
vendored
@@ -212,27 +212,17 @@ deploy:
|
||||
6. **No duplicate workflows**: Use reusable workflows for common tasks
|
||||
7. **No missing permissions**: Always specify required permissions
|
||||
|
||||
## Workflow Types
|
||||
## Workflow Types (Simplified)
|
||||
|
||||
### 1. CI Workflow (`ci.yml`)
|
||||
- Runs on every PR and push
|
||||
- Tests, linting, security scans
|
||||
- No deployments or publishing
|
||||
### 1. Pull Request (`pull-request.yml`)
|
||||
- Fast feedback loop
|
||||
- Lint, unit tests, basic security
|
||||
- Docker build only if relevant files changed
|
||||
|
||||
### 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
|
||||
### 2. Main Pipeline (`main.yml`)
|
||||
- Complete testing and deployment
|
||||
- Coverage reporting, security scans
|
||||
- Docker builds and publishing
|
||||
|
||||
## Checklist for New Workflows
|
||||
|
||||
|
||||
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -9,9 +9,9 @@ updates:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
reviewers:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
assignees:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
# Enable version updates for Docker
|
||||
@@ -23,9 +23,9 @@ updates:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
reviewers:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
assignees:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
|
||||
# Enable version updates for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
@@ -36,6 +36,6 @@ updates:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
reviewers:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
assignees:
|
||||
- "intelligence-assist"
|
||||
- "claude-did-this"
|
||||
304
.github/workflows/ci.yml
vendored
304
.github/workflows/ci.yml
vendored
@@ -1,304 +0,0 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# Lint job - fast and independent
|
||||
lint:
|
||||
name: Lint & Format Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint:check || echo "No lint script found, skipping"
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check || echo "No format script found, skipping"
|
||||
|
||||
# Unit tests - fastest test suite
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
# Integration tests - moderate complexity
|
||||
test-integration:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration || echo "No integration tests found, skipping"
|
||||
env:
|
||||
NODE_ENV: test
|
||||
BOT_USERNAME: '@TestBot'
|
||||
GITHUB_WEBHOOK_SECRET: 'test-secret'
|
||||
GITHUB_TOKEN: 'test-token'
|
||||
|
||||
|
||||
# Coverage generation - depends on unit tests
|
||||
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
|
||||
|
||||
# Security scans - run on GitHub for faster execution
|
||||
security:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Run npm audit
|
||||
run: npm audit --audit-level=moderate
|
||||
|
||||
- name: Run security scan with Snyk
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --severity-threshold=high
|
||||
|
||||
# Check if Docker-related files changed
|
||||
changes:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
src: ${{ steps.changes.outputs.src }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
docker:
|
||||
- 'Dockerfile*'
|
||||
- 'scripts/**'
|
||||
- '.dockerignore'
|
||||
- 'claude-config*'
|
||||
src:
|
||||
- 'src/**'
|
||||
- 'package*.json'
|
||||
|
||||
# Docker builds - only when relevant files change
|
||||
docker:
|
||||
name: Docker Build & Test
|
||||
runs-on: 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]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Start build profiling
|
||||
run: |
|
||||
echo "BUILD_START_TIME=$(date +%s)" >> $GITHUB_ENV
|
||||
echo "🏗️ Docker build started at $(date)"
|
||||
|
||||
- name: Set up Docker layer caching
|
||||
run: |
|
||||
# Create cache mount directories
|
||||
mkdir -p /tmp/.buildx-cache-main /tmp/.buildx-cache-claude
|
||||
|
||||
- name: Build main Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
tags: claude-github-webhook:test
|
||||
cache-from: |
|
||||
type=gha,scope=main
|
||||
type=local,src=/tmp/.buildx-cache-main
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=main
|
||||
type=local,dest=/tmp/.buildx-cache-main-new,mode=max
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
|
||||
- name: Build Claude Code Docker image (parallel)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.claudecode
|
||||
push: false
|
||||
load: true
|
||||
tags: claude-code-runner:test
|
||||
cache-from: |
|
||||
type=gha,scope=claudecode
|
||||
type=local,src=/tmp/.buildx-cache-claude
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=claudecode
|
||||
type=local,dest=/tmp/.buildx-cache-claude-new,mode=max
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BUILDKIT_INLINE_CACHE=1
|
||||
|
||||
- name: Rotate build caches
|
||||
run: |
|
||||
# Rotate caches to avoid size limits
|
||||
rm -rf /tmp/.buildx-cache-main /tmp/.buildx-cache-claude
|
||||
mv /tmp/.buildx-cache-main-new /tmp/.buildx-cache-main 2>/dev/null || true
|
||||
mv /tmp/.buildx-cache-claude-new /tmp/.buildx-cache-claude 2>/dev/null || true
|
||||
|
||||
- name: Profile build performance
|
||||
run: |
|
||||
BUILD_END_TIME=$(date +%s)
|
||||
BUILD_DURATION=$((BUILD_END_TIME - BUILD_START_TIME))
|
||||
echo "🏁 Docker build completed at $(date)"
|
||||
echo "⏱️ Total build time: ${BUILD_DURATION} seconds"
|
||||
|
||||
# Check image sizes
|
||||
echo "📦 Image sizes:"
|
||||
docker images | grep -E "(claude-github-webhook|claude-code-runner):test" || true
|
||||
|
||||
# Show cache usage
|
||||
echo "💾 Cache statistics:"
|
||||
du -sh /tmp/.buildx-cache-* 2>/dev/null || echo "No local caches found"
|
||||
|
||||
# Performance summary
|
||||
if [ $BUILD_DURATION -lt 120 ]; then
|
||||
echo "✅ Fast build (< 2 minutes)"
|
||||
elif [ $BUILD_DURATION -lt 300 ]; then
|
||||
echo "⚠️ Moderate build (2-5 minutes)"
|
||||
else
|
||||
echo "🐌 Slow build (> 5 minutes) - consider optimization"
|
||||
fi
|
||||
|
||||
- name: Test Docker containers
|
||||
run: |
|
||||
# Test main container starts correctly
|
||||
docker run --name test-webhook -d -p 3003:3002 \
|
||||
-e NODE_ENV=test \
|
||||
-e BOT_USERNAME=@TestBot \
|
||||
-e GITHUB_WEBHOOK_SECRET=test-secret \
|
||||
-e GITHUB_TOKEN=test-token \
|
||||
claude-github-webhook:test
|
||||
|
||||
# Wait for container to start
|
||||
sleep 10
|
||||
|
||||
# Test health endpoint
|
||||
curl -f http://localhost:3003/health || exit 1
|
||||
|
||||
# Cleanup
|
||||
docker stop test-webhook
|
||||
docker rm test-webhook
|
||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -154,7 +154,7 @@ jobs:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
# ============================================
|
||||
# CD Jobs - Run on self-hosted runners
|
||||
# CD Jobs - Run on GitHub-hosted runners
|
||||
# ============================================
|
||||
|
||||
deploy-staging:
|
||||
|
||||
31
.github/workflows/docker-publish.yml
vendored
31
.github/workflows/docker-publish.yml
vendored
@@ -16,13 +16,11 @@ env:
|
||||
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:
|
||||
# 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"]') }}
|
||||
# Always use GitHub-hosted runners
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -118,7 +116,7 @@ jobs:
|
||||
|
||||
# Build claudecode separately
|
||||
build-claudecode:
|
||||
runs-on: ${{ vars.USE_SELF_HOSTED == 'false' && 'ubuntu-latest' || fromJSON('["self-hosted", "linux", "x64", "docker"]') }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
@@ -170,25 +168,4 @@ jobs:
|
||||
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
|
||||
# Note: Fallback job removed since we're always using GitHub-hosted runners
|
||||
66
.github/workflows/main.yml
vendored
Normal file
66
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Main Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint:check
|
||||
- run: npm run test:ci
|
||||
env:
|
||||
NODE_ENV: test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/security/credential-audit.sh
|
||||
- uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
||||
head: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
extra_args: --debug --only-verified
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, security]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
360
.github/workflows/pr.yml
vendored
360
.github/workflows/pr.yml
vendored
@@ -1,360 +0,0 @@
|
||||
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!"
|
||||
40
.github/workflows/pull-request.yml
vendored
Normal file
40
.github/workflows/pull-request.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint:check
|
||||
- run: npm run test:unit
|
||||
env:
|
||||
NODE_ENV: test
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/security/credential-audit.sh
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.pull_request.changed_files, 'Dockerfile') || contains(github.event.pull_request.changed_files, 'src/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: test:latest
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules/
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.template
|
||||
!.env.quickstart
|
||||
|
||||
# Logs
|
||||
logs
|
||||
@@ -79,6 +80,8 @@ service-account.json
|
||||
|
||||
# Claude authentication output
|
||||
.claude-hub/
|
||||
claude-config/
|
||||
claude-config*
|
||||
|
||||
# Docker secrets
|
||||
secrets/
|
||||
@@ -93,4 +96,4 @@ secrets/
|
||||
# Root level clutter prevention
|
||||
/test-*.js
|
||||
/PR_SUMMARY.md
|
||||
/*-proposal.md
|
||||
/*-proposal.md
|
||||
|
||||
25
.husky/pre-commit
Executable file
25
.husky/pre-commit
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "🎨 Running Prettier check..."
|
||||
if ! npm run format:check; then
|
||||
echo "❌ Prettier formatting issues found!"
|
||||
echo "💡 Run 'npm run format' to fix formatting issues, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔍 Running ESLint check..."
|
||||
if ! npm run lint:check; then
|
||||
echo "❌ ESLint issues found!"
|
||||
echo "💡 Run 'npm run lint' to fix linting issues, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📝 Running TypeScript check..."
|
||||
if ! npm run typecheck; then
|
||||
echo "❌ TypeScript errors found!"
|
||||
echo "💡 Fix TypeScript errors, then commit again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All pre-commit checks passed!"
|
||||
@@ -1,39 +1,37 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-yaml
|
||||
- id: detect-private-key
|
||||
|
||||
- repo: https://github.com/Yelp/detect-secrets
|
||||
rev: v1.4.0
|
||||
rev: v1.5.0
|
||||
hooks:
|
||||
- id: detect-secrets
|
||||
args: ['--baseline', '.secrets.baseline']
|
||||
exclude: node_modules/
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.18.1
|
||||
exclude: package-lock.json
|
||||
|
||||
- repo: https://github.com/zricethezav/gitleaks
|
||||
rev: v8.21.2
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
- repo: https://github.com/thoughtworks/talisman
|
||||
rev: v1.32.0
|
||||
hooks:
|
||||
- id: talisman-commit
|
||||
entry: cmd --githook pre-commit
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: env-file-check
|
||||
name: Check for .env files
|
||||
entry: bash -c 'if find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" | grep -q .; then echo "Found .env files that may contain secrets"; exit 1; fi'
|
||||
- id: eslint
|
||||
name: eslint
|
||||
entry: npm run lint:check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
- id: credential-scan
|
||||
name: Scan for hardcoded credentials
|
||||
entry: bash -c 'if grep -r "sk-\|ghp_\|AKIA\|xox[boas]\|AIza[0-9A-Za-z\\-_]\{35\}" --exclude-dir=node_modules --exclude-dir=.git .; then echo "Found potential hardcoded credentials"; exit 1; fi'
|
||||
files: \.(js|ts)$
|
||||
|
||||
- id: prettier
|
||||
name: prettier
|
||||
entry: npm run format:check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: \.(js|ts|json|md)$
|
||||
14
CLAUDE.md
14
CLAUDE.md
@@ -46,7 +46,7 @@ This repository contains a webhook service that integrates Claude with GitHub, a
|
||||
- **View logs**: `docker compose logs -f webhook`
|
||||
- **Restart**: `docker compose restart webhook`
|
||||
- Build Claude container: `./build-claude-container.sh`
|
||||
- Build Claude Code container: `./scripts/build/build-claudecode.sh`
|
||||
- Build Claude Code container: `./scripts/build/build.sh claudecode`
|
||||
- Update production image: `./update-production-image.sh`
|
||||
|
||||
### AWS Credential Management
|
||||
@@ -71,10 +71,18 @@ This repository contains a webhook service that integrates Claude with GitHub, a
|
||||
- Fix security vulnerabilities: `npm run security:fix`
|
||||
- All CI tests: `npm run test:ci` (includes coverage)
|
||||
|
||||
### Pre-commit Hooks
|
||||
The project uses Husky for Git pre-commit hooks to ensure code quality:
|
||||
- **ESLint**: Checks code for linting errors
|
||||
- **Prettier**: Validates code formatting
|
||||
- **TypeScript**: Runs type checking
|
||||
- **Setup**: Hooks are automatically installed via `npm run prepare`
|
||||
- **Manual run**: Execute `.husky/pre-commit` to test locally
|
||||
|
||||
### End-to-End Testing
|
||||
Use the demo repository for testing auto-tagging and webhook functionality:
|
||||
- Demo repository: `https://github.com/intelligence-assist/demo-repository`
|
||||
- Test auto-tagging: `./cli/webhook-cli.js --repo "intelligence-assist/demo-repository" --command "Auto-tag this issue" --issue 1 --url "http://localhost:8082"`
|
||||
- Demo repository: `https://github.com/claude-did-this/demo-repository`
|
||||
- Test auto-tagging: `./cli/webhook-cli.js --repo "claude-did-this/demo-repository" --command "Auto-tag this issue" --issue 1 --url "http://localhost:8082"`
|
||||
- Test with specific issue content: Create a new issue in the demo repository to trigger auto-tagging webhook
|
||||
- Verify labels are applied based on issue content analysis
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ RUN apt update && apt install -y \
|
||||
curl \
|
||||
vim \
|
||||
nano \
|
||||
gh
|
||||
gh \
|
||||
rsync
|
||||
|
||||
# Set up npm global directory
|
||||
RUN mkdir -p /usr/local/share/npm-global && \
|
||||
@@ -32,34 +33,31 @@ RUN mkdir -p /auth-setup && chown -R node:node /auth-setup
|
||||
ENV SHELL /bin/zsh
|
||||
WORKDIR /auth-setup
|
||||
|
||||
# Create setup script that captures authentication state
|
||||
RUN cat > /setup-claude-auth.sh << 'EOF'
|
||||
# Create setup script
|
||||
COPY <<'EOF' /setup-claude-auth.sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔧 Claude Authentication Setup Container"
|
||||
echo "========================================"
|
||||
echo "🔧 Claude Authentication Setup"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
echo "This container allows you to authenticate with Claude interactively"
|
||||
echo "and capture the authentication state for use in other containers."
|
||||
echo "This will help you connect Claude to your account."
|
||||
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 "Quick setup - just run this command:"
|
||||
echo ""
|
||||
echo "The ~/.claude directory will be preserved in /auth-output"
|
||||
echo " claude --dangerously-skip-permissions && exit"
|
||||
echo ""
|
||||
echo "This will authenticate Claude and save your setup automatically."
|
||||
echo ""
|
||||
|
||||
# Function to copy authentication state
|
||||
copy_auth_state() {
|
||||
if [ -d "/home/node/.claude" ] && [ -d "/auth-output" ]; then
|
||||
echo "💾 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"
|
||||
echo "💾 Saving your authentication..."
|
||||
# Copy authentication files, excluding todos
|
||||
rsync -a --exclude='todos/' /home/node/.claude/ /auth-output/ 2>/dev/null || \
|
||||
cp -r /home/node/.claude/. /auth-output/ 2>/dev/null || true
|
||||
echo "✅ Authentication saved successfully!"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -70,21 +68,41 @@ trap copy_auth_state EXIT
|
||||
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 ""
|
||||
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
|
||||
'
|
||||
# Check if we should run automatically
|
||||
if [ "$1" = "--auto" ]; then
|
||||
echo "Running authentication automatically..."
|
||||
echo ""
|
||||
sudo -u node bash -c '
|
||||
export HOME=/home/node
|
||||
export PATH=/usr/local/share/npm-global/bin:$PATH
|
||||
cd /home/node
|
||||
claude --dangerously-skip-permissions
|
||||
exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Authentication command failed with exit code $exit_code"
|
||||
exit $exit_code
|
||||
fi
|
||||
'
|
||||
else
|
||||
# Switch to node user and start interactive shell
|
||||
sudo -u node bash -c '
|
||||
export HOME=/home/node
|
||||
export PATH=/usr/local/share/npm-global/bin:$PATH
|
||||
cd /home/node
|
||||
echo "Ready! Run this command to authenticate and exit:"
|
||||
echo ""
|
||||
echo " claude --dangerously-skip-permissions && exit"
|
||||
echo ""
|
||||
exec bash -i
|
||||
'
|
||||
fi
|
||||
EOF
|
||||
|
||||
RUN chmod +x /setup-claude-auth.sh
|
||||
|
||||
# Set entrypoint to setup script
|
||||
ENTRYPOINT ["/setup-claude-auth.sh"]
|
||||
ENTRYPOINT ["/bin/bash", "/setup-claude-auth.sh"]
|
||||
@@ -44,10 +44,11 @@ RUN npm install -g @anthropic-ai/claude-code
|
||||
# Switch back to root
|
||||
USER root
|
||||
|
||||
# Copy the pre-authenticated Claude config to BOTH root and node user
|
||||
COPY claude-config /root/.claude
|
||||
COPY claude-config /home/node/.claude
|
||||
RUN chown -R node:node /home/node/.claude
|
||||
# Copy the pre-authenticated Claude config to BOTH root and node user (only for production builds)
|
||||
# For regular builds, this will be empty directories that Claude can authenticate into
|
||||
# COPY claude-config /root/.claude
|
||||
# COPY claude-config /home/node/.claude
|
||||
# RUN chown -R node:node /home/node/.claude
|
||||
|
||||
# Copy the rest of the setup
|
||||
WORKDIR /workspace
|
||||
@@ -72,14 +73,12 @@ RUN chmod +x /usr/local/bin/init-firewall.sh && \
|
||||
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
|
||||
chmod 0440 /etc/sudoers.d/node-firewall
|
||||
|
||||
# Create scripts directory and copy entrypoint scripts
|
||||
# Create scripts directory and copy unified entrypoint script
|
||||
RUN mkdir -p /scripts/runtime
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /scripts/runtime/claudecode-entrypoint.sh
|
||||
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
|
||||
chmod +x /scripts/runtime/claudecode-entrypoint.sh
|
||||
|
||||
# Set the default shell to bash
|
||||
ENV SHELL /bin/zsh
|
||||
|
||||
141
QUICKSTART.md
Normal file
141
QUICKSTART.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 🚀 Quick Start Guide
|
||||
|
||||
Get Claude responding to your GitHub issues in minutes using Cloudflare Tunnel.
|
||||
|
||||
## Prerequisites
|
||||
- GitHub account
|
||||
- Docker installed
|
||||
- Claude.ai account with Max plan (5x or 20x)
|
||||
- Cloudflare account (free tier works)
|
||||
|
||||
## Step 1: Create a GitHub Bot Account
|
||||
|
||||
1. Sign out of GitHub and create a new account for your bot (e.g., `YourProjectBot`)
|
||||
2. In your main account, create a [Personal Access Token](https://github.com/settings/tokens) with `repo` and `write` permissions
|
||||
3. Add the bot account as a collaborator to your repositories
|
||||
|
||||
## Step 2: Clone and Configure
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/claude-did-this/claude-hub.git
|
||||
cd claude-hub
|
||||
|
||||
# Copy the quickstart environment file
|
||||
cp .env.quickstart .env
|
||||
|
||||
# Edit .env with your values
|
||||
nano .env
|
||||
```
|
||||
|
||||
Required values:
|
||||
- `GITHUB_TOKEN`: Your GitHub Personal Access Token
|
||||
- `GITHUB_WEBHOOK_SECRET`: Generate with `openssl rand -hex 32`
|
||||
- `BOT_USERNAME`: Your bot's GitHub username (e.g., `@YourProjectBot`)
|
||||
- `BOT_EMAIL`: Your bot's email
|
||||
- `AUTHORIZED_USERS`: Comma-separated GitHub usernames who can use the bot
|
||||
|
||||
## Step 3: Authenticate Claude
|
||||
|
||||
```bash
|
||||
# Run the interactive setup
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Open your browser for Claude.ai authentication
|
||||
2. Save your credentials securely
|
||||
3. Confirm everything is working
|
||||
|
||||
## Step 4: Start the Service
|
||||
|
||||
```bash
|
||||
# Start the webhook service
|
||||
docker compose up -d
|
||||
|
||||
# Check it's running
|
||||
docker compose logs -f webhook
|
||||
```
|
||||
|
||||
## Step 5: Install Cloudflare Tunnel
|
||||
|
||||
### Option A: Ubuntu/Debian
|
||||
```bash
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared focal main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||
|
||||
# Install cloudflared
|
||||
sudo apt-get update && sudo apt-get install cloudflared
|
||||
```
|
||||
|
||||
### Option B: Direct Download
|
||||
```bash
|
||||
# Download the latest cloudflared binary
|
||||
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
|
||||
sudo dpkg -i cloudflared-linux-amd64.deb
|
||||
```
|
||||
|
||||
### Option C: Using snap
|
||||
```bash
|
||||
sudo snap install cloudflared
|
||||
```
|
||||
|
||||
## Step 6: Create Tunnel
|
||||
|
||||
```bash
|
||||
# Create a tunnel to your local service
|
||||
cloudflared tunnel --url http://localhost:3002
|
||||
```
|
||||
|
||||
Copy the generated URL (like `https://abc123.trycloudflare.com`)
|
||||
|
||||
## Step 7: Configure GitHub Webhook
|
||||
|
||||
1. Go to your repository → Settings → Webhooks
|
||||
2. Click "Add webhook"
|
||||
3. **Payload URL**: Your Cloudflare URL + `/api/webhooks/github`
|
||||
- Example: `https://abc123.trycloudflare.com/api/webhooks/github`
|
||||
4. **Content type**: `application/json`
|
||||
5. **Secret**: Same value as `GITHUB_WEBHOOK_SECRET` in your .env
|
||||
6. **Events**: Select "Let me select individual events"
|
||||
- Check: Issues, Issue comments, Pull requests, Pull request reviews
|
||||
|
||||
## 🎉 You're Done!
|
||||
|
||||
Test it in your own repository by creating an issue and mentioning your bot:
|
||||
|
||||
```
|
||||
@YourProjectBot Can you help me understand this codebase?
|
||||
```
|
||||
|
||||
**Note:** Your bot will only respond in repositories where you've configured the webhook and to users listed in `AUTHORIZED_USERS`.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Production Deployment**: Set up a permanent Cloudflare Tunnel with `cloudflared service install`
|
||||
- **Advanced Features**: Check `.env.example` for PR auto-review, auto-tagging, and more
|
||||
- **Multiple Repos**: Add the same webhook to any repo where you want bot assistance
|
||||
|
||||
## Community & Support
|
||||
|
||||
[](https://discord.gg/yb7hwQjTFg)
|
||||
[](https://claude-did-this.com/claude-hub/overview)
|
||||
|
||||
Join our Discord server for help, updates, and to share your experience!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bot not responding?**
|
||||
- Check logs: `docker compose logs webhook`
|
||||
- Verify webhook delivery in GitHub → Settings → Webhooks → Recent Deliveries
|
||||
- Ensure the commenting user is in `AUTHORIZED_USERS`
|
||||
|
||||
**Authentication issues?**
|
||||
- Re-run: `./scripts/setup/setup-claude-interactive.sh`
|
||||
- Ensure you have an active Claude.ai Max plan (5x or 20x)
|
||||
|
||||
**Need help?** Ask in our [Discord server](https://discord.gg/yb7hwQjTFg) or check the [full documentation](https://claude-did-this.com/claude-hub/overview)!
|
||||
82
README.md
82
README.md
@@ -1,14 +1,17 @@
|
||||
# Claude GitHub Webhook
|
||||
|
||||
[](https://github.com/intelligence-assist/claude-hub/actions/workflows/ci.yml)
|
||||
[](https://github.com/intelligence-assist/claude-hub/actions/workflows/security.yml)
|
||||
[](https://discord.com/widget?id=1377708770209304676&theme=dark)
|
||||
[](https://github.com/claude-did-this/claude-hub/actions/workflows/main.yml)
|
||||
[](https://github.com/claude-did-this/claude-hub/actions/workflows/security.yml)
|
||||
[](test/README.md)
|
||||
[](https://codecov.io/gh/intelligence-assist/claude-hub)
|
||||
[](https://github.com/intelligence-assist/claude-hub/releases)
|
||||
[](https://codecov.io/gh/claude-did-this/claude-hub)
|
||||
[](https://github.com/claude-did-this/claude-hub/releases)
|
||||
[](https://hub.docker.com/r/intelligenceassist/claude-hub)
|
||||
[](package.json)
|
||||
[](LICENSE)
|
||||
|
||||
🚀 **[Quick Start Guide](./QUICKSTART.md)** | 💬 **[Discord](https://discord.com/widget?id=1377708770209304676&theme=dark)** | 📚 **[Documentation](https://claude-did-this.com/claude-hub/overview)** | 📖 **[Complete Setup](./docs/complete-workflow.md)** | 🔐 **[Authentication](./docs/claude-authentication-guide.md)**
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -25,6 +28,29 @@ Deploy Claude Code as a fully autonomous GitHub bot. Create your own bot account
|
||||
|
||||
Claude autonomously handles complete development workflows. It analyzes your entire repository, implements features from scratch, conducts thorough code reviews, manages pull requests, monitors CI/CD pipelines, and responds to automated feedback - all without human intervention. No context switching. No manual oversight required. Just seamless autonomous development where you work.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
**Follow our [10-minute Quick Start Guide](./QUICKSTART.md)** to get Claude responding to your GitHub issues using Cloudflare Tunnel - no domain or complex setup required!
|
||||
|
||||
```bash
|
||||
# 1. Clone and configure
|
||||
git clone https://github.com/claude-did-this/claude-hub.git
|
||||
cd claude-hub
|
||||
cp .env.quickstart .env
|
||||
nano .env # Add your GitHub token and bot details
|
||||
|
||||
# 2. Authenticate Claude (uses your Claude.ai Max subscription)
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
# 3. Start the service
|
||||
docker compose up -d
|
||||
|
||||
# 4. Create a tunnel (see quickstart guide for details)
|
||||
cloudflared tunnel --url http://localhost:3002
|
||||
```
|
||||
|
||||
That's it! Your bot is ready to use. See the **[complete quickstart guide](./QUICKSTART.md)** for detailed instructions and webhook setup.
|
||||
|
||||
## Autonomous Workflow Capabilities
|
||||
|
||||
### End-to-End Development 🚀
|
||||
@@ -64,44 +90,6 @@ Claude autonomously handles complete development workflows. It analyzes your ent
|
||||
- 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
|
||||
|
||||
@@ -153,8 +141,8 @@ Use your existing Claude Max subscription for automation instead of pay-per-use
|
||||
./scripts/setup/setup-claude-interactive.sh
|
||||
|
||||
# 2. In container: authenticate with your subscription
|
||||
claude login # Follow browser flow
|
||||
exit # Save authentication
|
||||
claude --dangerously-skip-permissions # Follow authentication flow
|
||||
exit # Save authentication
|
||||
|
||||
# 3. Use captured authentication
|
||||
cp -r ${CLAUDE_HUB_DIR:-~/.claude-hub}/* ~/.claude/
|
||||
@@ -214,7 +202,7 @@ AWS_SECRET_ACCESS_KEY=xxx
|
||||
Integrate Claude without GitHub webhooks:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8082/api/claude \
|
||||
curl -X POST http://localhost:3002/api/claude \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"repoFullName": "owner/repo",
|
||||
@@ -307,7 +295,7 @@ CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8082/health
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
### Logs
|
||||
@@ -393,7 +381,7 @@ npm run dev
|
||||
|
||||
### Support
|
||||
|
||||
- Report issues: [GitHub Issues](https://github.com/intelligence-assist/claude-hub/issues)
|
||||
- Report issues: [GitHub Issues](https://github.com/claude-did-this/claude-hub/issues)
|
||||
- Detailed troubleshooting: [Complete Workflow Guide](./docs/complete-workflow.md#troubleshooting)
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{"parentUuid":null,"isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","type":"user","message":{"role":"user","content":"auth"},"uuid":"5bea393c-77c6-4f32-ac62-a157e0159045","timestamp":"2025-05-19T01:19:11.851Z"}
|
||||
{"parentUuid":"5bea393c-77c6-4f32-ac62-a157e0159045","isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","message":{"id":"msg_bdrk_01Lz7rrWgXdzbMayCabnExTJ","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"text","text":"I'll search for authentication-related files and code in the repository."},{"type":"tool_use","id":"toolu_bdrk_01FCr4cpVZtKEZ1E9TD6AXcr","name":"Task","input":{"description":"Find auth files","prompt":"Search for any authentication-related files, code, or implementations in the repository. Look for files with names containing \"auth\", authentication implementations, login functionality, or security-related code. Return a list of relevant files and a brief summary of what each one contains."}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":17318,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":136}},"costUSD":0.053994,"durationMs":5319,"type":"assistant","uuid":"5df3af64-5b6c-457f-b559-9741977e06f5","timestamp":"2025-05-19T01:19:17.209Z"}
|
||||
{"parentUuid":"5df3af64-5b6c-457f-b559-9741977e06f5","isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"[Request interrupted by user for tool use]","is_error":true,"tool_use_id":"toolu_bdrk_01FCr4cpVZtKEZ1E9TD6AXcr"}]},"uuid":"84e6bfdd-e508-459d-b0b8-d02ccada8f5f","timestamp":"2025-05-19T01:19:21.315Z","toolUseResult":"Error: [Request interrupted by user for tool use]"}
|
||||
{"parentUuid":"84e6bfdd-e508-459d-b0b8-d02ccada8f5f","isSidechain":false,"userType":"external","cwd":"/workspace","sessionId":"d4460a3e-0af0-4e8c-a3c5-0427c9620fab","version":"0.2.118","type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user for tool use]"}]},"uuid":"ffe5b08f-786c-4cc7-9271-fead3ca72f4f","timestamp":"2025-05-19T01:19:21.319Z"}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -2,19 +2,18 @@ services:
|
||||
webhook:
|
||||
build: .
|
||||
ports:
|
||||
- "8082:3003"
|
||||
- "${PORT:-3002}:${PORT:-3002}"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${HOME}/.aws:/root/.aws:ro
|
||||
- ${HOME}/.claude-hub:/home/node/.claude
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3003
|
||||
- PORT=${PORT:-3002}
|
||||
- TRUST_PROXY=${TRUST_PROXY:-true}
|
||||
- AUTHORIZED_USERS=${AUTHORIZED_USERS:-Cheffromspace}
|
||||
- BOT_USERNAME=${BOT_USERNAME:-@MCPClaude}
|
||||
- BOT_EMAIL=${BOT_EMAIL:-claude@example.com}
|
||||
- DEFAULT_GITHUB_OWNER=${DEFAULT_GITHUB_OWNER:-Cheffromspace}
|
||||
- DEFAULT_GITHUB_USER=${DEFAULT_GITHUB_USER:-Cheffromspace}
|
||||
- DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}
|
||||
@@ -22,6 +21,9 @@ services:
|
||||
- CLAUDE_CONTAINER_IMAGE=claudecode:latest
|
||||
- CLAUDE_AUTH_HOST_DIR=${CLAUDE_AUTH_HOST_DIR:-${HOME}/.claude-hub}
|
||||
- DISABLE_LOG_REDACTION=true
|
||||
# Claude Code timeout settings for unattended mode
|
||||
- BASH_DEFAULT_TIMEOUT_MS=${BASH_DEFAULT_TIMEOUT_MS:-600000} # 10 minutes default
|
||||
- BASH_MAX_TIMEOUT_MS=${BASH_MAX_TIMEOUT_MS:-1200000} # 20 minutes max
|
||||
# Smart wait for all meaningful checks by default, or use specific workflow trigger
|
||||
- PR_REVIEW_WAIT_FOR_ALL_CHECKS=${PR_REVIEW_WAIT_FOR_ALL_CHECKS:-true}
|
||||
- PR_REVIEW_TRIGGER_WORKFLOW=${PR_REVIEW_TRIGGER_WORKFLOW:-}
|
||||
@@ -34,7 +36,7 @@ services:
|
||||
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3003/health"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:${PORT:-3002}/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
204
docs/environment-variables.md
Normal file
204
docs/environment-variables.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Environment Variables Documentation
|
||||
|
||||
This document provides a comprehensive list of all environment variables used in the Claude GitHub Webhook project.
|
||||
|
||||
## Table of Contents
|
||||
- [Core Application Configuration](#core-application-configuration)
|
||||
- [Bot Configuration](#bot-configuration)
|
||||
- [GitHub Configuration](#github-configuration)
|
||||
- [Claude/Anthropic Configuration](#claudeanthropic-configuration)
|
||||
- [Container Configuration](#container-configuration)
|
||||
- [AWS Configuration](#aws-configuration)
|
||||
- [PR Review Configuration](#pr-review-configuration)
|
||||
- [Security & Secrets Configuration](#security--secrets-configuration)
|
||||
- [Rate Limiting Configuration](#rate-limiting-configuration)
|
||||
- [Health Check Configuration](#health-check-configuration)
|
||||
- [Development/Test Variables](#developmenttest-variables)
|
||||
- [Shell Script Variables](#shell-script-variables)
|
||||
- [Hard-coded Values That Could Be Configurable](#hard-coded-values-that-could-be-configurable)
|
||||
|
||||
## Core Application Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `NODE_ENV` | Application environment (development/production/test) | `development` | No |
|
||||
| `PORT` | Server port | `3002` | No |
|
||||
| `TRUST_PROXY` | Trust proxy headers for X-Forwarded-For | `false` | No |
|
||||
|
||||
## Bot Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `BOT_USERNAME` | GitHub username the bot responds to (e.g., @ClaudeBot) | - | Yes |
|
||||
| `BOT_EMAIL` | Email used for git commits by the bot | - | Yes |
|
||||
| `DEFAULT_AUTHORIZED_USER` | Default authorized GitHub username | - | No |
|
||||
| `AUTHORIZED_USERS` | Comma-separated list of authorized GitHub usernames | - | No |
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `GITHUB_TOKEN` | GitHub personal access token | - | Yes |
|
||||
| `GITHUB_WEBHOOK_SECRET` | Secret for validating GitHub webhook payloads | - | Yes |
|
||||
| `DEFAULT_GITHUB_OWNER` | Default GitHub organization/owner | - | No |
|
||||
| `DEFAULT_GITHUB_USER` | Default GitHub username | - | No |
|
||||
| `DEFAULT_BRANCH` | Default git branch | `main` | No |
|
||||
| `TEST_REPO_FULL_NAME` | Test repository in owner/repo format | - | No |
|
||||
|
||||
## Claude/Anthropic Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `ANTHROPIC_API_KEY` | Anthropic API key for Claude access | - | Yes* |
|
||||
| `ANTHROPIC_MODEL` | Model name | `us.anthropic.claude-3-7-sonnet-20250219-v1:0` | No |
|
||||
| `CLAUDE_CODE_USE_BEDROCK` | Whether to use AWS Bedrock for Claude (0/1) | `0` | No |
|
||||
| `CLAUDE_HUB_DIR` | Directory for Claude Hub config | `~/.claude-hub` | No |
|
||||
| `CLAUDE_AUTH_HOST_DIR` | Host directory for Claude authentication | - | No |
|
||||
|
||||
*Required unless using AWS Bedrock or setup container authentication
|
||||
|
||||
## Container Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `CLAUDE_USE_CONTAINERS` | Enable container execution (0/1) | `1` | No |
|
||||
| `CLAUDE_CONTAINER_IMAGE` | Docker image for Claude containers | `claudecode:latest` | No |
|
||||
| `CLAUDE_CONTAINER_PRIVILEGED` | Run containers in privileged mode | `false` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_NET_RAW` | Add NET_RAW capability | `true` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_SYS_TIME` | Add SYS_TIME capability | `false` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_DAC_OVERRIDE` | Add DAC_OVERRIDE capability | `true` | No |
|
||||
| `CLAUDE_CONTAINER_CAP_AUDIT_WRITE` | Add AUDIT_WRITE capability | `true` | No |
|
||||
| `CLAUDE_CONTAINER_CPU_SHARES` | CPU shares for containers | `1024` | No |
|
||||
| `CLAUDE_CONTAINER_MEMORY_LIMIT` | Memory limit for containers | `2g` | No |
|
||||
| `CLAUDE_CONTAINER_PIDS_LIMIT` | Process limit for containers | `256` | No |
|
||||
| `CONTAINER_LIFETIME_MS` | Container execution timeout in milliseconds | `7200000` (2 hours) | No |
|
||||
| `REPO_CACHE_DIR` | Directory for repository cache | `/tmp/repo-cache` | No |
|
||||
| `REPO_CACHE_MAX_AGE_MS` | Max age for cached repos in milliseconds | `3600000` (1 hour) | No |
|
||||
|
||||
## Claude Code Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `BASH_DEFAULT_TIMEOUT_MS` | Default timeout for bash commands in Claude Code | `600000` (10 minutes) | No |
|
||||
| `BASH_MAX_TIMEOUT_MS` | Maximum timeout Claude can set for bash commands | `1200000` (20 minutes) | No |
|
||||
|
||||
## AWS Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `AWS_ACCESS_KEY_ID` | AWS access key ID | - | No* |
|
||||
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key | - | No* |
|
||||
| `AWS_SESSION_TOKEN` | AWS session token (for temporary credentials) | - | No |
|
||||
| `AWS_SECURITY_TOKEN` | Alternative name for session token | - | No |
|
||||
| `AWS_REGION` | AWS region | `us-east-1` | No |
|
||||
| `AWS_PROFILE` | AWS profile name | - | No |
|
||||
| `USE_AWS_PROFILE` | Use AWS profile instead of direct credentials | `false` | No |
|
||||
| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` | ECS container credentials URI | - | No |
|
||||
|
||||
*Required if using AWS Bedrock for Claude
|
||||
|
||||
## PR Review Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `PR_REVIEW_WAIT_FOR_ALL_CHECKS` | Wait for all checks before PR review | `true` | No |
|
||||
| `PR_REVIEW_TRIGGER_WORKFLOW` | Specific workflow name to trigger PR review | - | No |
|
||||
| `PR_REVIEW_DEBOUNCE_MS` | Delay before checking all check suites | `5000` | No |
|
||||
| `PR_REVIEW_MAX_WAIT_MS` | Max wait for in-progress checks | `1800000` (30 min) | No |
|
||||
| `PR_REVIEW_CONDITIONAL_TIMEOUT_MS` | Timeout for conditional jobs | `300000` (5 min) | No |
|
||||
|
||||
## Security & Secrets Configuration
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `GITHUB_TOKEN_FILE` | Path to file containing GitHub token | `/run/secrets/github_token` | No |
|
||||
| `ANTHROPIC_API_KEY_FILE` | Path to file containing Anthropic API key | `/run/secrets/anthropic_api_key` | No |
|
||||
| `GITHUB_WEBHOOK_SECRET_FILE` | Path to file containing webhook secret | `/run/secrets/webhook_secret` | No |
|
||||
| `DISABLE_LOG_REDACTION` | Disable credential redaction in logs | `false` | No |
|
||||
|
||||
## Rate Limiting Configuration
|
||||
|
||||
These values are currently hard-coded but could be made configurable:
|
||||
|
||||
| Value | Description | Current Value | Location |
|
||||
|-------|-------------|---------------|----------|
|
||||
| Rate limit window | API rate limit time window | 15 minutes | `src/index.ts:32` |
|
||||
| Rate limit max requests | Max API requests per window | 100 | `src/index.ts:41` |
|
||||
| Webhook rate limit window | Webhook rate limit time window | 5 minutes | `src/index.ts:50` |
|
||||
| Webhook rate limit max requests | Max webhook requests per window | 50 | `src/index.ts:51` |
|
||||
|
||||
## Health Check Configuration
|
||||
|
||||
These values are defined in docker-compose.yml:
|
||||
|
||||
| Value | Description | Current Value |
|
||||
|-------|-------------|---------------|
|
||||
| Health check interval | Time between health checks | 30s |
|
||||
| Health check timeout | Timeout for each health check | 10s |
|
||||
| Health check retries | Number of retries before unhealthy | 3 |
|
||||
| Health check start period | Grace period on startup | 10s |
|
||||
|
||||
## Development/Test Variables
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `API_URL` | API URL for testing | `http://localhost:3003` | No |
|
||||
| `WEBHOOK_URL` | Webhook URL for testing | - | No |
|
||||
| `CLAUDE_API_AUTH_REQUIRED` | Require auth for Claude API | `false` | No |
|
||||
| `CLAUDE_API_AUTH_TOKEN` | Auth token for Claude API | - | No |
|
||||
| `HOME` | User home directory | - | No |
|
||||
| `WORKSPACE_PATH` | GitHub Actions workspace path | - | No |
|
||||
| `GITHUB_WORKSPACE` | GitHub Actions workspace | - | No |
|
||||
|
||||
## Shell Script Variables
|
||||
|
||||
| Variable | Description | Used In |
|
||||
|----------|-------------|---------|
|
||||
| `ALLOWED_TOOLS` | Tools allowed for Claude execution | entrypoint scripts |
|
||||
| `OPERATION_TYPE` | Type of operation (tagging, review, etc.) | entrypoint scripts |
|
||||
| `PRODUCTION_BOT` | Production bot username | setup scripts |
|
||||
| `STAGING_BOT` | Staging bot username | setup scripts |
|
||||
| `RUNNER_TOKEN` | GitHub Actions runner token | runner scripts |
|
||||
|
||||
## Hard-coded Values That Could Be Configurable
|
||||
|
||||
The following values are currently hard-coded in the source code but could potentially be made configurable via environment variables:
|
||||
|
||||
### Buffer Sizes
|
||||
- Docker execution buffer: 10MB (`src/services/claudeService.ts:160`)
|
||||
- Container logs buffer: 1MB (`src/services/claudeService.ts:184,590`)
|
||||
|
||||
### External URLs
|
||||
- EC2 metadata endpoint: `http://169.254.169.254/latest/meta-data/` (`src/utils/awsCredentialProvider.ts:94`)
|
||||
- GitHub API meta: `https://api.github.com/meta` (`scripts/security/init-firewall.sh:32`)
|
||||
|
||||
### Allowed Domains (Firewall)
|
||||
- `registry.npmjs.org`
|
||||
- `api.anthropic.com`
|
||||
- `sentry.io`
|
||||
- `statsig.anthropic.com`
|
||||
- `statsig.com`
|
||||
|
||||
### Default Values
|
||||
- Default git email in containers: `claude@example.com` (`scripts/runtime/claudecode-entrypoint.sh:89`)
|
||||
- Default git username in containers: `ClaudeBot` (`scripts/runtime/claudecode-entrypoint.sh:90`)
|
||||
- Health check container image: `claude-code-runner:latest` (`src/index.ts:140`)
|
||||
|
||||
### Docker Base Images
|
||||
- Node base image: `node:24` (`Dockerfile.claudecode:1`)
|
||||
- Delta version: `0.18.2` (`Dockerfile.claudecode:87`)
|
||||
- Zsh-in-docker version: `v1.2.0` (`Dockerfile.claudecode:91`)
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Secret Files**: The application supports loading secrets from files, which takes priority over environment variables. This is more secure for production deployments.
|
||||
|
||||
2. **AWS Authentication**: The service supports multiple AWS authentication methods:
|
||||
- Direct credentials (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)
|
||||
- AWS profiles (AWS_PROFILE with USE_AWS_PROFILE=true)
|
||||
- Instance profiles (EC2)
|
||||
- Task roles (ECS)
|
||||
|
||||
3. **Container Capabilities**: The container capability flags allow fine-grained control over container permissions for security purposes.
|
||||
|
||||
4. **Staging Environment**: Additional environment variables are defined in `.env.staging` for staging deployments, following the pattern `VARIABLE_NAME_STAGING`.
|
||||
@@ -1,9 +1,11 @@
|
||||
const js = require('@eslint/js');
|
||||
const tseslint = require('@typescript-eslint/eslint-plugin');
|
||||
const tsparser = require('@typescript-eslint/parser');
|
||||
const prettierConfig = require('eslint-config-prettier');
|
||||
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
prettierConfig, // Disable all formatting rules that conflict with Prettier
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@@ -34,11 +36,7 @@ module.exports = [
|
||||
'no-console': 'warn',
|
||||
'no-debugger': 'error',
|
||||
|
||||
// Code style
|
||||
'indent': ['error', 2],
|
||||
'quotes': ['error', 'single'],
|
||||
'semi': ['error', 'always'],
|
||||
'comma-dangle': ['error', 'never'],
|
||||
// Removed all formatting rules - let Prettier handle them
|
||||
|
||||
// Best practices
|
||||
'eqeqeq': 'error',
|
||||
|
||||
@@ -18,6 +18,11 @@ module.exports = {
|
||||
collectCoverage: true,
|
||||
coverageReporters: ['text', 'lcov'],
|
||||
coverageDirectory: 'coverage',
|
||||
coveragePathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/dist/',
|
||||
'/coverage/'
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,ts}',
|
||||
'!src/**/*.d.ts',
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: claude-webhook-secrets
|
||||
namespace: default
|
||||
type: Opaque
|
||||
stringData:
|
||||
github-token: "YOUR_GITHUB_TOKEN_HERE"
|
||||
anthropic-api-key: "YOUR_ANTHROPIC_API_KEY_HERE"
|
||||
webhook-secret: "YOUR_WEBHOOK_SECRET_HERE"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: claude-webhook
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: claude-webhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: claude-webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
image: claude-webhook:latest
|
||||
ports:
|
||||
- containerPort: 3002
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "3002"
|
||||
- name: GITHUB_TOKEN_FILE
|
||||
value: "/etc/secrets/github-token"
|
||||
- name: ANTHROPIC_API_KEY_FILE
|
||||
value: "/etc/secrets/anthropic-api-key"
|
||||
- name: GITHUB_WEBHOOK_SECRET_FILE
|
||||
value: "/etc/secrets/webhook-secret"
|
||||
volumeMounts:
|
||||
- name: secrets-volume
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: secrets-volume
|
||||
secret:
|
||||
secretName: claude-webhook-secrets
|
||||
items:
|
||||
- key: github-token
|
||||
path: github-token
|
||||
- key: anthropic-api-key
|
||||
path: anthropic-api-key
|
||||
- key: webhook-secret
|
||||
path: webhook-secret
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-github-webhook",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-github-webhook",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"axios": "^1.6.2",
|
||||
@@ -33,6 +33,7 @@
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-node": "^4.1.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
@@ -5995,6 +5996,22 @@
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
|
||||
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-config-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-github-webhook",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "A webhook endpoint for Claude to perform git and GitHub actions",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
@@ -29,7 +29,9 @@
|
||||
"format:check": "prettier --check src/ test/",
|
||||
"security:audit": "npm audit --audit-level=moderate",
|
||||
"security:fix": "npm audit fix",
|
||||
"setup:dev": "husky install"
|
||||
"setup:dev": "husky install",
|
||||
"setup:hooks": "husky",
|
||||
"prepare": "husky || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
@@ -57,6 +59,7 @@
|
||||
"babel-jest": "^29.7.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-node": "^4.1.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
|
||||
@@ -14,7 +14,7 @@ case "$BUILD_TYPE" in
|
||||
|
||||
claudecode)
|
||||
echo "Building Claude Code runner Docker image..."
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:latest .
|
||||
docker build -f Dockerfile.claudecode -t claudecode:latest .
|
||||
;;
|
||||
|
||||
production)
|
||||
@@ -25,10 +25,106 @@ case "$BUILD_TYPE" in
|
||||
fi
|
||||
|
||||
echo "Building production image with pre-authenticated config..."
|
||||
cp Dockerfile.claudecode Dockerfile.claudecode.backup
|
||||
# Production build logic from update-production-image.sh
|
||||
# ... (truncated for brevity)
|
||||
docker build -f Dockerfile.claudecode -t claude-code-runner:production .
|
||||
|
||||
# Create a temporary production Dockerfile with claude-config enabled
|
||||
cat > Dockerfile.claudecode.prod << 'EOF'
|
||||
FROM node:24
|
||||
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y less \
|
||||
git \
|
||||
procps \
|
||||
sudo \
|
||||
fzf \
|
||||
zsh \
|
||||
man-db \
|
||||
unzip \
|
||||
gnupg2 \
|
||||
gh \
|
||||
iptables \
|
||||
ipset \
|
||||
iproute2 \
|
||||
dnsutils \
|
||||
aggregate \
|
||||
jq
|
||||
|
||||
# Set up npm global directory
|
||||
RUN mkdir -p /usr/local/share/npm-global && \
|
||||
chown -R node:node /usr/local/share
|
||||
|
||||
# Configure zsh and command history
|
||||
ENV USERNAME=node
|
||||
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
||||
&& mkdir /commandhistory \
|
||||
&& touch /commandhistory/.bash_history \
|
||||
&& chown -R $USERNAME /commandhistory
|
||||
|
||||
# Create workspace and config directories
|
||||
RUN mkdir -p /workspace /home/node/.claude && \
|
||||
chown -R node:node /workspace /home/node/.claude
|
||||
|
||||
# Switch to node user temporarily for npm install
|
||||
USER node
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
|
||||
# Install Claude Code
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Switch back to root
|
||||
USER root
|
||||
|
||||
# Copy the pre-authenticated Claude config to BOTH root and node user (PRODUCTION ONLY)
|
||||
COPY claude-config /root/.claude
|
||||
COPY claude-config /home/node/.claude
|
||||
RUN chown -R node:node /home/node/.claude
|
||||
|
||||
# Copy the rest of the setup
|
||||
WORKDIR /workspace
|
||||
|
||||
# Install delta and zsh
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \
|
||||
sudo dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \
|
||||
rm "git-delta_0.18.2_${ARCH}.deb"
|
||||
|
||||
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \
|
||||
-p git \
|
||||
-p fzf \
|
||||
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
|
||||
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
|
||||
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
||||
-x
|
||||
|
||||
# Copy firewall and entrypoint scripts
|
||||
COPY scripts/security/init-firewall.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/init-firewall.sh && \
|
||||
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
|
||||
chmod 0440 /etc/sudoers.d/node-firewall
|
||||
|
||||
# Create scripts directory and copy unified entrypoint script
|
||||
RUN mkdir -p /scripts/runtime
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY scripts/runtime/claudecode-entrypoint.sh /scripts/runtime/claudecode-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh && \
|
||||
chmod +x /scripts/runtime/claudecode-entrypoint.sh
|
||||
|
||||
# Set the default shell to bash
|
||||
ENV SHELL /bin/zsh
|
||||
ENV DEVCONTAINER=true
|
||||
|
||||
# Run as root to allow permission management
|
||||
USER root
|
||||
|
||||
# Use the custom entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
EOF
|
||||
|
||||
# Build the production image
|
||||
docker build -f Dockerfile.claudecode.prod -t claudecode:production .
|
||||
|
||||
# Clean up temporary file
|
||||
rm -f Dockerfile.claudecode.prod
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# GitHub Actions Runner Management Script
|
||||
# Manage the webhook deployment runner service
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
SERVICE_NAME="webhook-deployment-runner"
|
||||
RUNNER_DIR="/home/jonflatt/github-actions-runner"
|
||||
RUNNER_USER="jonflatt"
|
||||
|
||||
# Function to print usage
|
||||
usage() {
|
||||
echo -e "${BLUE}GitHub Actions Runner Management Tool${NC}"
|
||||
echo -e "${BLUE}=====================================${NC}"
|
||||
echo -e "\nUsage: $0 [command]"
|
||||
echo -e "\nCommands:"
|
||||
echo -e " ${GREEN}start${NC} - Start the runner service"
|
||||
echo -e " ${GREEN}stop${NC} - Stop the runner service"
|
||||
echo -e " ${GREEN}restart${NC} - Restart the runner service"
|
||||
echo -e " ${GREEN}status${NC} - Check runner service status"
|
||||
echo -e " ${GREEN}logs${NC} - View runner logs (live)"
|
||||
echo -e " ${GREEN}logs-tail${NC} - View last 50 lines of logs"
|
||||
echo -e " ${GREEN}update${NC} - Update runner to latest version"
|
||||
echo -e " ${GREEN}config${NC} - Show runner configuration"
|
||||
echo -e " ${GREEN}health${NC} - Check runner health"
|
||||
echo -e " ${GREEN}jobs${NC} - Show recent job history"
|
||||
echo -e " ${GREEN}cleanup${NC} - Clean up work directory"
|
||||
echo -e " ${GREEN}info${NC} - Show runner information"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if running with correct permissions
|
||||
check_permissions() {
|
||||
if [[ $EUID -ne 0 ]] && [[ "$1" =~ ^(start|stop|restart|update)$ ]]; then
|
||||
echo -e "${RED}Error: This command requires sudo privileges${NC}"
|
||||
echo -e "${YELLOW}Run: sudo $0 $1${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the runner
|
||||
start_runner() {
|
||||
echo -e "${YELLOW}Starting runner service...${NC}"
|
||||
systemctl start $SERVICE_NAME
|
||||
sleep 2
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}✓ Runner started successfully${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager | head -n 10
|
||||
else
|
||||
echo -e "${RED}✗ Failed to start runner${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop the runner
|
||||
stop_runner() {
|
||||
echo -e "${YELLOW}Stopping runner service...${NC}"
|
||||
systemctl stop $SERVICE_NAME
|
||||
echo -e "${GREEN}✓ Runner stopped${NC}"
|
||||
}
|
||||
|
||||
# Restart the runner
|
||||
restart_runner() {
|
||||
echo -e "${YELLOW}Restarting runner service...${NC}"
|
||||
systemctl restart $SERVICE_NAME
|
||||
sleep 2
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}✓ Runner restarted successfully${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager | head -n 10
|
||||
else
|
||||
echo -e "${RED}✗ Failed to restart runner${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check runner status
|
||||
check_status() {
|
||||
echo -e "${BLUE}Runner Service Status${NC}"
|
||||
echo -e "${BLUE}===================${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager
|
||||
|
||||
echo -e "\n${BLUE}Runner Process Info${NC}"
|
||||
echo -e "${BLUE}===================${NC}"
|
||||
ps aux | grep -E "(Runner.Listener|run.sh)" | grep -v grep || echo "No runner processes found"
|
||||
}
|
||||
|
||||
# View logs
|
||||
view_logs() {
|
||||
echo -e "${YELLOW}Viewing live logs (Ctrl+C to exit)...${NC}"
|
||||
journalctl -u $SERVICE_NAME -f
|
||||
}
|
||||
|
||||
# View last 50 lines of logs
|
||||
view_logs_tail() {
|
||||
echo -e "${BLUE}Last 50 lines of runner logs${NC}"
|
||||
echo -e "${BLUE}===========================${NC}"
|
||||
journalctl -u $SERVICE_NAME -n 50 --no-pager
|
||||
}
|
||||
|
||||
# Update runner
|
||||
update_runner() {
|
||||
echo -e "${YELLOW}Updating GitHub Actions Runner...${NC}"
|
||||
|
||||
# Stop the service
|
||||
systemctl stop $SERVICE_NAME
|
||||
|
||||
# Get current version
|
||||
CURRENT_VERSION=$($RUNNER_DIR/bin/Runner.Listener --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown")
|
||||
echo -e "Current version: ${YELLOW}$CURRENT_VERSION${NC}"
|
||||
|
||||
# Get latest version
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
|
||||
echo -e "Latest version: ${GREEN}$LATEST_VERSION${NC}"
|
||||
|
||||
if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then
|
||||
echo -e "${GREEN}✓ Runner is already up to date${NC}"
|
||||
systemctl start $SERVICE_NAME
|
||||
return
|
||||
fi
|
||||
|
||||
# Backup current runner
|
||||
echo -e "${YELLOW}Backing up current runner...${NC}"
|
||||
cd $RUNNER_DIR
|
||||
tar -czf runner-backup-$(date +%Y%m%d-%H%M%S).tar.gz bin externals
|
||||
|
||||
# Download and extract new version
|
||||
echo -e "${YELLOW}Downloading new version...${NC}"
|
||||
curl -o actions-runner-linux-x64.tar.gz -L "https://github.com/actions/runner/releases/download/v${LATEST_VERSION}/actions-runner-linux-x64-${LATEST_VERSION}.tar.gz"
|
||||
tar xzf ./actions-runner-linux-x64.tar.gz
|
||||
rm actions-runner-linux-x64.tar.gz
|
||||
|
||||
# Start the service
|
||||
systemctl start $SERVICE_NAME
|
||||
echo -e "${GREEN}✓ Runner updated to version $LATEST_VERSION${NC}"
|
||||
}
|
||||
|
||||
# Show configuration
|
||||
show_config() {
|
||||
echo -e "${BLUE}Runner Configuration${NC}"
|
||||
echo -e "${BLUE}===================${NC}"
|
||||
|
||||
if [ -f "$RUNNER_DIR/.runner" ]; then
|
||||
echo -e "\n${GREEN}Runner Settings:${NC}"
|
||||
cat "$RUNNER_DIR/.runner" | jq '.' 2>/dev/null || cat "$RUNNER_DIR/.runner"
|
||||
fi
|
||||
|
||||
if [ -f "$RUNNER_DIR/.credentials" ]; then
|
||||
echo -e "\n${GREEN}Runner Registration:${NC}"
|
||||
echo "Runner is registered (credentials file exists)"
|
||||
else
|
||||
echo -e "\n${RED}Runner is not configured${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}Service Configuration:${NC}"
|
||||
systemctl show $SERVICE_NAME | grep -E "(LoadState|ActiveState|SubState|MainPID|Environment)"
|
||||
}
|
||||
|
||||
# Check health
|
||||
check_health() {
|
||||
echo -e "${BLUE}Runner Health Check${NC}"
|
||||
echo -e "${BLUE}==================${NC}"
|
||||
|
||||
# Check service status
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN}✓ Service is running${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Service is not running${NC}"
|
||||
fi
|
||||
|
||||
# Check disk space
|
||||
DISK_USAGE=$(df -h $RUNNER_DIR | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||
if [ "$DISK_USAGE" -lt 80 ]; then
|
||||
echo -e "${GREEN}✓ Disk usage: ${DISK_USAGE}%${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Disk usage: ${DISK_USAGE}% (High)${NC}"
|
||||
fi
|
||||
|
||||
# Check work directory size
|
||||
if [ -d "$RUNNER_DIR/_work" ]; then
|
||||
WORK_SIZE=$(du -sh "$RUNNER_DIR/_work" 2>/dev/null | cut -f1)
|
||||
echo -e "${BLUE}Work directory size: $WORK_SIZE${NC}"
|
||||
fi
|
||||
|
||||
# Check runner connectivity
|
||||
if [ -f "$RUNNER_DIR/.runner" ]; then
|
||||
GITHUB_URL=$(cat "$RUNNER_DIR/.runner" | jq -r '.gitHubUrl' 2>/dev/null || echo "")
|
||||
if [ -n "$GITHUB_URL" ] && curl -s -o /dev/null -w "%{http_code}" "$GITHUB_URL" | grep -q "200"; then
|
||||
echo -e "${GREEN}✓ GitHub connectivity OK${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Cannot verify GitHub connectivity${NC}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Show recent jobs
|
||||
show_jobs() {
|
||||
echo -e "${BLUE}Recent Runner Jobs${NC}"
|
||||
echo -e "${BLUE}=================${NC}"
|
||||
|
||||
# Check for job history in work directory
|
||||
if [ -d "$RUNNER_DIR/_work" ]; then
|
||||
echo -e "\n${GREEN}Recent job directories:${NC}"
|
||||
ls -la "$RUNNER_DIR/_work" 2>/dev/null | tail -n 10 || echo "No job directories found"
|
||||
fi
|
||||
|
||||
# Show recent log entries
|
||||
echo -e "\n${GREEN}Recent job activity:${NC}"
|
||||
journalctl -u $SERVICE_NAME --since "1 hour ago" | grep -E "(Running job|Job .* completed|Completed request)" | tail -n 20 || echo "No recent job activity"
|
||||
}
|
||||
|
||||
# Cleanup work directory
|
||||
cleanup_work() {
|
||||
echo -e "${YELLOW}Cleaning up work directory...${NC}"
|
||||
|
||||
if [ ! -d "$RUNNER_DIR/_work" ]; then
|
||||
echo -e "${GREEN}Work directory doesn't exist${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Show current size
|
||||
BEFORE_SIZE=$(du -sh "$RUNNER_DIR/_work" 2>/dev/null | cut -f1)
|
||||
echo -e "Current size: ${YELLOW}$BEFORE_SIZE${NC}"
|
||||
|
||||
# Confirm
|
||||
read -p "Are you sure you want to clean the work directory? (y/N): " confirm
|
||||
if [ "$confirm" != "y" ]; then
|
||||
echo -e "${YELLOW}Cleanup cancelled${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Stop runner
|
||||
systemctl stop $SERVICE_NAME
|
||||
|
||||
# Clean work directory
|
||||
rm -rf "$RUNNER_DIR/_work"/*
|
||||
|
||||
# Start runner
|
||||
systemctl start $SERVICE_NAME
|
||||
|
||||
echo -e "${GREEN}✓ Work directory cleaned${NC}"
|
||||
}
|
||||
|
||||
# Show runner info
|
||||
show_info() {
|
||||
echo -e "${BLUE}GitHub Actions Runner Information${NC}"
|
||||
echo -e "${BLUE}=================================${NC}"
|
||||
|
||||
echo -e "\n${GREEN}Basic Info:${NC}"
|
||||
echo -e "Service Name: ${YELLOW}$SERVICE_NAME${NC}"
|
||||
echo -e "Runner Directory: ${YELLOW}$RUNNER_DIR${NC}"
|
||||
echo -e "Runner User: ${YELLOW}$RUNNER_USER${NC}"
|
||||
|
||||
if [ -f "$RUNNER_DIR/bin/Runner.Listener" ]; then
|
||||
VERSION=$($RUNNER_DIR/bin/Runner.Listener --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "unknown")
|
||||
echo -e "Runner Version: ${YELLOW}$VERSION${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}System Info:${NC}"
|
||||
echo -e "Hostname: ${YELLOW}$(hostname)${NC}"
|
||||
echo -e "OS: ${YELLOW}$(lsb_release -d | cut -f2)${NC}"
|
||||
echo -e "Kernel: ${YELLOW}$(uname -r)${NC}"
|
||||
echo -e "Architecture: ${YELLOW}$(uname -m)${NC}"
|
||||
|
||||
echo -e "\n${GREEN}Docker Info:${NC}"
|
||||
if command -v docker &> /dev/null; then
|
||||
DOCKER_VERSION=$(docker --version | awk '{print $3}' | sed 's/,$//')
|
||||
echo -e "Docker Version: ${YELLOW}$DOCKER_VERSION${NC}"
|
||||
|
||||
if groups $RUNNER_USER | grep -q docker; then
|
||||
echo -e "Docker Access: ${GREEN}✓ User in docker group${NC}"
|
||||
else
|
||||
echo -e "Docker Access: ${RED}✗ User not in docker group${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Docker not installed${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}Labels:${NC}"
|
||||
echo -e "${YELLOW}self-hosted,linux,x64,deployment,webhook-cd${NC}"
|
||||
}
|
||||
|
||||
# Main logic
|
||||
check_permissions "$1"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_runner
|
||||
;;
|
||||
stop)
|
||||
stop_runner
|
||||
;;
|
||||
restart)
|
||||
restart_runner
|
||||
;;
|
||||
status)
|
||||
check_status
|
||||
;;
|
||||
logs)
|
||||
view_logs
|
||||
;;
|
||||
logs-tail)
|
||||
view_logs_tail
|
||||
;;
|
||||
update)
|
||||
update_runner
|
||||
;;
|
||||
config)
|
||||
show_config
|
||||
;;
|
||||
health)
|
||||
check_health
|
||||
;;
|
||||
jobs)
|
||||
show_jobs
|
||||
;;
|
||||
cleanup)
|
||||
cleanup_work
|
||||
;;
|
||||
info)
|
||||
show_info
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Unified entrypoint for Claude Code operations
|
||||
# Handles both auto-tagging (minimal tools) and general operations (full tools)
|
||||
# Operation type is controlled by OPERATION_TYPE environment variable
|
||||
|
||||
# Initialize firewall - must be done as root
|
||||
# Temporarily disabled to test Claude Code
|
||||
# /usr/local/bin/init-firewall.sh
|
||||
@@ -68,8 +72,12 @@ else
|
||||
cd /workspace
|
||||
fi
|
||||
|
||||
# Checkout the correct branch
|
||||
if [ "${IS_PULL_REQUEST}" = "true" ] && [ -n "${BRANCH_NAME}" ]; then
|
||||
# Checkout the correct branch based on operation type
|
||||
if [ "${OPERATION_TYPE}" = "auto-tagging" ]; then
|
||||
# Auto-tagging always uses main branch (doesn't need specific branches)
|
||||
echo "Using main branch for auto-tagging" >&2
|
||||
sudo -u node git checkout main >&2 || sudo -u node git checkout master >&2
|
||||
elif [ "${IS_PULL_REQUEST}" = "true" ] && [ -n "${BRANCH_NAME}" ]; then
|
||||
echo "Checking out PR branch: ${BRANCH_NAME}" >&2
|
||||
sudo -u node git checkout "${BRANCH_NAME}" >&2
|
||||
else
|
||||
@@ -107,8 +115,20 @@ RESPONSE_FILE="/workspace/response.txt"
|
||||
touch "${RESPONSE_FILE}"
|
||||
chown node:node "${RESPONSE_FILE}"
|
||||
|
||||
# Run Claude Code with full GitHub CLI access as node user
|
||||
echo "Running Claude Code..." >&2
|
||||
# Determine allowed tools based on operation type
|
||||
if [ "${OPERATION_TYPE}" = "auto-tagging" ]; then
|
||||
ALLOWED_TOOLS="Read,GitHub,Bash(gh issue edit:*),Bash(gh issue view:*),Bash(gh label list:*)" # Minimal tools for auto-tagging (security)
|
||||
echo "Running Claude Code for auto-tagging with minimal tools..." >&2
|
||||
elif [ "${OPERATION_TYPE}" = "pr-review" ] || [ "${OPERATION_TYPE}" = "manual-pr-review" ]; then
|
||||
# PR Review: Broad research access + controlled write access
|
||||
# Read access: Full file system, git history, GitHub data
|
||||
# Write access: GitHub comments/reviews, PR labels, but no file deletion/modification
|
||||
ALLOWED_TOOLS="Read,GitHub,Bash(gh:*),Bash(git log:*),Bash(git show:*),Bash(git diff:*),Bash(git blame:*),Bash(find:*),Bash(grep:*),Bash(rg:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(ls:*),Bash(tree:*)"
|
||||
echo "Running Claude Code for PR review with broad research access..." >&2
|
||||
else
|
||||
ALLOWED_TOOLS="Bash,Create,Edit,Read,Write,GitHub" # Full tools for general operations
|
||||
echo "Running Claude Code with full tool access..." >&2
|
||||
fi
|
||||
|
||||
# Check if command exists
|
||||
if [ -z "${COMMAND}" ]; then
|
||||
@@ -134,8 +154,12 @@ sudo -u node -E env \
|
||||
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
|
||||
GH_TOKEN="${GITHUB_TOKEN}" \
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
||||
BASH_DEFAULT_TIMEOUT_MS="${BASH_DEFAULT_TIMEOUT_MS}" \
|
||||
BASH_MAX_TIMEOUT_MS="${BASH_MAX_TIMEOUT_MS}" \
|
||||
/usr/local/share/npm-global/bin/claude \
|
||||
--allowedTools Bash,Create,Edit,Read,Write,GitHub \
|
||||
--allowedTools "${ALLOWED_TOOLS}" \
|
||||
--verbose \
|
||||
--print "${COMMAND}" \
|
||||
> "${RESPONSE_FILE}" 2>&1
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/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}"
|
||||
@@ -1,7 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get port from environment or default to 3003
|
||||
DEFAULT_PORT=${PORT:-3003}
|
||||
# Load environment variables from .env file if it exists
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Get port from environment or default to 3002
|
||||
DEFAULT_PORT=${PORT:-3002}
|
||||
|
||||
# Kill any processes using the port
|
||||
echo "Checking for existing processes on port $DEFAULT_PORT..."
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
echo "Starting Claude GitHub webhook service..."
|
||||
|
||||
# Build the Claude Code runner image
|
||||
echo "Building Claude Code runner image..."
|
||||
if docker build -f Dockerfile.claudecode -t claude-code-runner:latest .; then
|
||||
echo "Claude Code runner image built successfully."
|
||||
# Build the Claude Code runner image if we have access to Dockerfile.claudecode
|
||||
if [ -f "Dockerfile.claudecode" ]; then
|
||||
echo "Building Claude Code runner image..."
|
||||
if docker build -f Dockerfile.claudecode -t claude-code-runner:latest .; then
|
||||
echo "Claude Code runner image built successfully."
|
||||
else
|
||||
echo "Warning: Failed to build Claude Code runner image. Service will attempt to build on first use."
|
||||
fi
|
||||
else
|
||||
echo "Warning: Failed to build Claude Code runner image. Service will attempt to build on first use."
|
||||
echo "Dockerfile.claudecode not found, skipping Claude Code runner image build."
|
||||
fi
|
||||
|
||||
# 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
|
||||
# In production, dist directory is already built in the Docker image
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "Error: dist directory not found. Please rebuild the Docker image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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 dist/index.js
|
||||
@@ -32,8 +32,8 @@ report_success() {
|
||||
|
||||
# 1. Check for .env files that shouldn't be committed
|
||||
echo "🔍 Checking for exposed .env files..."
|
||||
if find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" | grep -q .; then
|
||||
find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" | while read file; do
|
||||
if find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" -not -name ".env.quickstart" | grep -q .; then
|
||||
find . -name ".env*" -not -path "./node_modules/*" -not -name ".env.example" -not -name ".env.template" -not -name ".env.quickstart" | while read file; do
|
||||
report_issue "Found .env file that may contain secrets: $file"
|
||||
done
|
||||
else
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Setting up Claude Code authentication..."
|
||||
|
||||
# Build the setup container
|
||||
docker build -f Dockerfile.setup -t claude-setup .
|
||||
|
||||
# Run it interactively with AWS credentials mounted
|
||||
docker run -it -v $HOME/.aws:/root/.aws:ro claude-setup
|
||||
|
||||
echo ""
|
||||
echo "After completing the authentication in the container:"
|
||||
echo "1. Run 'docker ps -a' to find the container ID"
|
||||
echo "2. Run 'docker cp <container_id>:/root/.claude ./claude-config'"
|
||||
echo "3. Then run './update-production-image.sh'"
|
||||
@@ -19,48 +19,76 @@ 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 "🚀 Starting Claude authentication..."
|
||||
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 "What happens next:"
|
||||
echo " 1. Claude will open your browser for authentication"
|
||||
echo " 2. Complete the authentication in your browser"
|
||||
echo " 3. Return here when done - the container will exit automatically"
|
||||
echo ""
|
||||
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..."
|
||||
read -p "Press Enter to start authentication..."
|
||||
|
||||
# Run the interactive container
|
||||
# Run the container with automatic authentication
|
||||
docker run -it --rm \
|
||||
-v "$AUTH_OUTPUT_DIR:/auth-output" \
|
||||
-v "$HOME/.gitconfig:/home/node/.gitconfig:ro" \
|
||||
--name claude-auth-setup \
|
||||
claude-setup:latest
|
||||
claude-setup:latest --auto
|
||||
|
||||
# Capture the exit code
|
||||
DOCKER_EXIT_CODE=$?
|
||||
|
||||
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"
|
||||
# First check if docker command failed
|
||||
if [ $DOCKER_EXIT_CODE -ne 0 ]; then
|
||||
echo "❌ Authentication process failed (exit code: $DOCKER_EXIT_CODE)"
|
||||
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"
|
||||
echo "Please check the error messages above and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if authentication was successful
|
||||
if [ -f "$AUTH_OUTPUT_DIR/.credentials.json" ]; then
|
||||
# Get file size
|
||||
FILE_SIZE=$(stat -f%z "$AUTH_OUTPUT_DIR/.credentials.json" 2>/dev/null || stat -c%s "$AUTH_OUTPUT_DIR/.credentials.json" 2>/dev/null || echo "0")
|
||||
|
||||
# Check if file has reasonable content (at least 100 bytes for a valid JSON)
|
||||
if [ "$FILE_SIZE" -gt 100 ]; then
|
||||
# Check if file was written recently (within last 5 minutes)
|
||||
if [ "$(find "$AUTH_OUTPUT_DIR/.credentials.json" -mmin -5 2>/dev/null)" ]; then
|
||||
echo "✅ Success! Your Claude authentication is saved."
|
||||
echo ""
|
||||
echo "The webhook service will use this automatically when you run:"
|
||||
echo " docker compose up -d"
|
||||
echo ""
|
||||
exit 0
|
||||
else
|
||||
echo "⚠️ Found old authentication files. The authentication may not have completed."
|
||||
echo "Please run the setup again to refresh your authentication."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Authentication file is too small (${FILE_SIZE} bytes). The authentication did not complete."
|
||||
echo ""
|
||||
echo "Common causes:"
|
||||
echo " - Browser authentication was cancelled"
|
||||
echo " - Network connection issues"
|
||||
echo " - Claude Code subscription not active"
|
||||
echo ""
|
||||
echo "Please run the setup again and complete the browser authentication."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Authentication failed - no credentials were saved."
|
||||
echo ""
|
||||
echo "This can happen if:"
|
||||
echo " - The browser authentication was not completed"
|
||||
echo " - The container exited before authentication finished"
|
||||
echo " - There was an error during the authentication process"
|
||||
echo ""
|
||||
echo "Please run './scripts/setup/setup-claude-interactive.sh' again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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'"
|
||||
@@ -385,6 +385,11 @@ async function handlePullRequestComment(
|
||||
if (commandMatch?.[1]) {
|
||||
const command = commandMatch[1].trim();
|
||||
|
||||
// Check for manual review command
|
||||
if (command.toLowerCase() === 'review') {
|
||||
return await handleManualPRReview(pr, repo, payload.sender, res);
|
||||
}
|
||||
|
||||
try {
|
||||
// Process the command with Claude
|
||||
logger.info('Sending command to Claude service');
|
||||
@@ -490,6 +495,30 @@ async function processBotMention(
|
||||
if (commandMatch?.[1]) {
|
||||
const command = commandMatch[1].trim();
|
||||
|
||||
// Check if this is a PR and the command is "review"
|
||||
if (command.toLowerCase() === 'review') {
|
||||
// Check if this is already a PR object
|
||||
if ('head' in issue && 'base' in issue) {
|
||||
return await handleManualPRReview(issue, repo, comment.user, res);
|
||||
}
|
||||
|
||||
// Check if this issue is actually a PR (GitHub includes pull_request property for PR comments)
|
||||
const issueWithPR = issue;
|
||||
if (issueWithPR.pull_request) {
|
||||
// Create a mock PR object from the issue data for the review
|
||||
const mockPR: GitHubPullRequest = {
|
||||
...issue,
|
||||
head: {
|
||||
ref: issueWithPR.pull_request.head?.ref ?? 'unknown',
|
||||
sha: issueWithPR.pull_request.head?.sha ?? 'unknown'
|
||||
},
|
||||
base: issueWithPR.pull_request.base ?? { ref: 'main' }
|
||||
} as GitHubPullRequest;
|
||||
|
||||
return await handleManualPRReview(mockPR, repo, comment.user, res);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Process the command with Claude
|
||||
logger.info('Sending command to Claude service');
|
||||
@@ -530,6 +559,211 @@ async function processBotMention(
|
||||
return res.status(200).json({ message: 'Webhook processed successfully' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle manual PR review requests via @botaccount review command
|
||||
*/
|
||||
async function handleManualPRReview(
|
||||
pr: GitHubPullRequest,
|
||||
repo: GitHubRepository,
|
||||
sender: { login: string },
|
||||
res: Response<WebhookResponse | ErrorResponse>
|
||||
): Promise<Response<WebhookResponse | ErrorResponse>> {
|
||||
try {
|
||||
// Check if the sender is authorized to trigger reviews
|
||||
const authorizedUsers = process.env.AUTHORIZED_USERS
|
||||
? process.env.AUTHORIZED_USERS.split(',').map(user => user.trim())
|
||||
: [process.env.DEFAULT_AUTHORIZED_USER ?? 'admin'];
|
||||
|
||||
if (!authorizedUsers.includes(sender.login)) {
|
||||
logger.info(
|
||||
{
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login
|
||||
},
|
||||
'Unauthorized user attempted to trigger manual PR review'
|
||||
);
|
||||
|
||||
try {
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`❌ Sorry @${sender.login}, only authorized users can trigger PR reviews.`
|
||||
);
|
||||
|
||||
await postComment({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
issueNumber: pr.number,
|
||||
body: errorMessage
|
||||
});
|
||||
} catch (commentError) {
|
||||
logger.error({ err: commentError }, 'Failed to post unauthorized review attempt comment');
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Unauthorized user - review request ignored',
|
||||
context: {
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login,
|
||||
branch: pr.head.ref,
|
||||
commitSha: pr.head.sha
|
||||
},
|
||||
'Processing manual PR review request'
|
||||
);
|
||||
|
||||
// Add "review-in-progress" label
|
||||
try {
|
||||
await managePRLabels({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
prNumber: pr.number,
|
||||
labelsToAdd: ['claude-review-in-progress'],
|
||||
labelsToRemove: ['claude-review-needed', 'claude-review-complete']
|
||||
});
|
||||
} catch (labelError) {
|
||||
logger.error(
|
||||
{
|
||||
err: (labelError as Error).message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number
|
||||
},
|
||||
'Failed to add review-in-progress label for manual review'
|
||||
);
|
||||
// Continue with review even if label fails
|
||||
}
|
||||
|
||||
// Create the PR review prompt
|
||||
const prReviewPrompt = createPRReviewPrompt(pr.number, repo.full_name, pr.head.sha);
|
||||
|
||||
// Process the PR review with Claude
|
||||
logger.info('Sending PR for manual Claude review');
|
||||
const claudeResponse = await processCommand({
|
||||
repoFullName: repo.full_name,
|
||||
issueNumber: pr.number,
|
||||
command: prReviewPrompt,
|
||||
isPullRequest: true,
|
||||
branchName: pr.head.ref,
|
||||
operationType: 'manual-pr-review'
|
||||
});
|
||||
|
||||
logger.info(
|
||||
{
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login,
|
||||
responseLength: claudeResponse ? claudeResponse.length : 0
|
||||
},
|
||||
'Manual PR review completed successfully'
|
||||
);
|
||||
|
||||
// Update label to show review is complete
|
||||
try {
|
||||
await managePRLabels({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
prNumber: pr.number,
|
||||
labelsToAdd: ['claude-review-complete'],
|
||||
labelsToRemove: ['claude-review-in-progress', 'claude-review-needed']
|
||||
});
|
||||
} catch (labelError) {
|
||||
logger.error(
|
||||
{
|
||||
err: (labelError as Error).message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number
|
||||
},
|
||||
'Failed to update review-complete label after manual review'
|
||||
);
|
||||
// Don't fail the review if label update fails
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Manual PR review completed successfully',
|
||||
context: {
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
type: 'manual_pr_review',
|
||||
sender: sender.login,
|
||||
branch: pr.head.ref
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.error(
|
||||
{
|
||||
err: err.message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
sender: sender.login
|
||||
},
|
||||
'Error processing manual PR review'
|
||||
);
|
||||
|
||||
// Remove in-progress label on error
|
||||
try {
|
||||
await managePRLabels({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
prNumber: pr.number,
|
||||
labelsToRemove: ['claude-review-in-progress']
|
||||
});
|
||||
} catch (labelError) {
|
||||
logger.error(
|
||||
{
|
||||
err: (labelError as Error).message,
|
||||
repo: repo.full_name,
|
||||
pr: pr.number
|
||||
},
|
||||
'Failed to remove review-in-progress label after manual review error'
|
||||
);
|
||||
}
|
||||
|
||||
// Post error comment
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
const errorId = `err-${Math.random().toString(36).substring(2, 10)}`;
|
||||
|
||||
const errorMessage = sanitizeBotMentions(
|
||||
`❌ An error occurred while processing the manual review request. (Reference: ${errorId}, Time: ${timestamp})
|
||||
|
||||
Please check with an administrator to review the logs for more details.`
|
||||
);
|
||||
|
||||
await postComment({
|
||||
repoOwner: repo.owner.login,
|
||||
repoName: repo.name,
|
||||
issueNumber: pr.number,
|
||||
body: errorMessage
|
||||
});
|
||||
} catch (commentError) {
|
||||
logger.error({ err: commentError }, 'Failed to post manual review error comment');
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to process manual PR review',
|
||||
message: err.message,
|
||||
context: {
|
||||
repo: repo.full_name,
|
||||
pr: pr.number,
|
||||
type: 'manual_pr_review_error',
|
||||
sender: sender.login
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command processing errors
|
||||
*/
|
||||
@@ -822,7 +1056,8 @@ async function processAutomatedPRReviews(
|
||||
issueNumber: pr.number,
|
||||
command: prReviewPrompt,
|
||||
isPullRequest: true,
|
||||
branchName: pr.head.ref
|
||||
branchName: pr.head.ref,
|
||||
operationType: 'pr-review'
|
||||
});
|
||||
|
||||
logger.info(
|
||||
|
||||
30
src/index.ts
30
src/index.ts
@@ -6,11 +6,7 @@ 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 type { WebhookRequest, HealthCheckResponse, ErrorResponse } from './types/express';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const app = express();
|
||||
@@ -22,7 +18,7 @@ if (trustProxy) {
|
||||
app.set('trust proxy', true);
|
||||
}
|
||||
|
||||
const PORT = parseInt(process.env['PORT'] ?? '3003', 10);
|
||||
const PORT = parseInt(process.env['PORT'] ?? '3002', 10);
|
||||
const appLogger = createLogger('app');
|
||||
const startupMetrics = new StartupMetrics();
|
||||
|
||||
@@ -31,26 +27,32 @@ startupMetrics.recordMilestone('env_loaded', 'Environment variables loaded');
|
||||
startupMetrics.recordMilestone('express_initialized', 'Express app initialized');
|
||||
|
||||
// Rate limiting configuration
|
||||
const generalRateLimit = rateLimit({
|
||||
// When behind a proxy, we need to properly handle client IP detection
|
||||
const rateLimitConfig = {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
// Skip validation when behind proxy to avoid startup errors
|
||||
validate: trustProxy ? false : undefined
|
||||
};
|
||||
|
||||
const generalRateLimit = rateLimit({
|
||||
...rateLimitConfig,
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: {
|
||||
error: 'Too many requests',
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false // Disable the `X-RateLimit-*` headers
|
||||
}
|
||||
});
|
||||
|
||||
const webhookRateLimit = rateLimit({
|
||||
...rateLimitConfig,
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 50, // Limit each IP to 50 webhook requests per 5 minutes
|
||||
message: {
|
||||
error: 'Too many webhook requests',
|
||||
message: 'Too many webhook requests from this IP, please try again later.'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: _req => {
|
||||
// Skip rate limiting in test environment
|
||||
return process.env['NODE_ENV'] === 'test';
|
||||
@@ -134,8 +136,9 @@ app.get('/health', (req: WebhookRequest, res: express.Response<HealthCheckRespon
|
||||
|
||||
// Check Claude Code runner image
|
||||
const imageCheckStart = Date.now();
|
||||
const dockerImageName = process.env['CLAUDE_CONTAINER_IMAGE'] ?? 'claudecode:latest';
|
||||
try {
|
||||
execSync('docker image inspect claude-code-runner:latest', { stdio: 'ignore' });
|
||||
execSync(`docker image inspect ${dockerImageName}`, { stdio: 'ignore' });
|
||||
checks.claudeCodeImage.available = true;
|
||||
} catch {
|
||||
checks.claudeCodeImage.error = 'Image not found';
|
||||
@@ -151,7 +154,6 @@ app.get('/health', (req: WebhookRequest, res: express.Response<HealthCheckRespon
|
||||
res.status(200).json(checks);
|
||||
});
|
||||
|
||||
|
||||
// Error handling middleware
|
||||
app.use(
|
||||
(
|
||||
|
||||
@@ -56,7 +56,8 @@ export async function processCommand({
|
||||
|
||||
// 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_'));
|
||||
const isValidGitHubToken =
|
||||
githubToken && (githubToken.includes('ghp_') || githubToken.includes('github_pat_'));
|
||||
if (process.env['NODE_ENV'] === 'test' || !isValidGitHubToken) {
|
||||
logger.info(
|
||||
{
|
||||
@@ -94,8 +95,8 @@ For real functionality, please configure valid GitHub and Claude API tokens.`;
|
||||
});
|
||||
}
|
||||
|
||||
// Select appropriate entrypoint script based on operation type
|
||||
const entrypointScript = getEntrypointScript(operationType);
|
||||
// Use unified entrypoint script for all operation types
|
||||
const entrypointScript = getEntrypointScript();
|
||||
logger.info(
|
||||
{ operationType },
|
||||
`Using ${operationType === 'auto-tagging' ? 'minimal tools for auto-tagging operation' : 'full tool set for standard operation'}`
|
||||
@@ -225,17 +226,11 @@ For real functionality, please configure valid GitHub and Claude API tokens.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate entrypoint script based on operation type
|
||||
* Get entrypoint script for Claude Code execution
|
||||
* Uses unified entrypoint that handles all operation types based on OPERATION_TYPE env var
|
||||
*/
|
||||
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';
|
||||
}
|
||||
function getEntrypointScript(): string {
|
||||
return '/scripts/runtime/claudecode-entrypoint.sh';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,7 +281,7 @@ ${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.
|
||||
return `You are ${process.env.BOT_USERNAME}, an AI assistant responding to a GitHub ${isPullRequest ? 'pull request' : 'issue'}.
|
||||
|
||||
**Context:**
|
||||
- Repository: ${repoFullName}
|
||||
@@ -353,7 +348,9 @@ function createEnvironmentVars({
|
||||
OPERATION_TYPE: operationType,
|
||||
COMMAND: fullPrompt,
|
||||
GITHUB_TOKEN: githubToken,
|
||||
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY') ?? ''
|
||||
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY') ?? '',
|
||||
BOT_USERNAME: process.env.BOT_USERNAME,
|
||||
BOT_EMAIL: process.env.BOT_EMAIL
|
||||
};
|
||||
}
|
||||
|
||||
@@ -386,8 +383,8 @@ function buildDockerArgs({
|
||||
if (hostAuthDir) {
|
||||
// Resolve relative paths to absolute paths for Docker volume mounting
|
||||
const path = require('path');
|
||||
const absoluteAuthDir = path.isAbsolute(hostAuthDir)
|
||||
? hostAuthDir
|
||||
const absoluteAuthDir = path.isAbsolute(hostAuthDir)
|
||||
? hostAuthDir
|
||||
: path.resolve(process.cwd(), hostAuthDir);
|
||||
dockerArgs.push('-v', `${absoluteAuthDir}:/home/node/.claude`);
|
||||
}
|
||||
|
||||
@@ -598,10 +598,10 @@ export async function getCheckSuitesForRef({
|
||||
conclusion: suite.conclusion,
|
||||
app: suite.app
|
||||
? {
|
||||
id: suite.app.id,
|
||||
slug: suite.app.slug,
|
||||
name: suite.app.name
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type OperationType = 'auto-tagging' | 'pr-review' | 'default';
|
||||
export type OperationType = 'auto-tagging' | 'pr-review' | 'manual-pr-review' | 'default';
|
||||
|
||||
export interface ClaudeCommandOptions {
|
||||
repoFullName: string;
|
||||
@@ -41,6 +41,8 @@ export interface ClaudeEnvironmentVars {
|
||||
COMMAND: string;
|
||||
GITHUB_TOKEN: string;
|
||||
ANTHROPIC_API_KEY: string;
|
||||
BOT_USERNAME?: string;
|
||||
BOT_EMAIL?: string;
|
||||
}
|
||||
|
||||
export interface DockerExecutionOptions {
|
||||
|
||||
@@ -56,7 +56,6 @@ export interface HealthCheckResponse {
|
||||
healthCheckDuration?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
message?: string;
|
||||
|
||||
@@ -18,6 +18,15 @@ export interface GitHubIssue {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
html_url: string;
|
||||
pull_request?: {
|
||||
head?: {
|
||||
ref: string;
|
||||
sha: string;
|
||||
};
|
||||
base?: {
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubPullRequest {
|
||||
|
||||
@@ -20,33 +20,33 @@ 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'
|
||||
targets: [
|
||||
// File transport for production
|
||||
{
|
||||
target: 'pino/file',
|
||||
options: { destination: logFileName, mkdir: true }
|
||||
},
|
||||
level: 'info'
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
// Just use pretty logs in development
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: true,
|
||||
translateTime: 'SYS:standard'
|
||||
// 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({
|
||||
|
||||
@@ -74,7 +74,7 @@ export function validateGitHubRef(ref: string): boolean {
|
||||
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);
|
||||
|
||||
@@ -2,36 +2,38 @@
|
||||
|
||||
## 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:
|
||||
The following shell test scripts have been migrated to the Jest E2E test suite and have been removed:
|
||||
|
||||
### AWS Tests
|
||||
### Migrated Shell Scripts (✅ Completed)
|
||||
|
||||
- `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`
|
||||
**AWS Tests** (Directory: `test/aws/` - removed)
|
||||
|
||||
### Claude Tests
|
||||
- `test-aws-mount.sh` → `test/e2e/scenarios/aws-authentication.test.js`
|
||||
- `test-aws-profile.sh` → `test/e2e/scenarios/aws-authentication.test.js`
|
||||
|
||||
- `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`
|
||||
**Claude Tests** (Directory: `test/claude/` - removed)
|
||||
|
||||
### Container Tests
|
||||
- `test-claude-direct.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-installation.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-no-firewall.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
- `test-claude-response.sh` → `test/e2e/scenarios/claude-integration.test.js`
|
||||
|
||||
- `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`
|
||||
**Container Tests** (Directory: `test/container/` - removed)
|
||||
|
||||
### Security Tests
|
||||
- `test-basic-container.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test-container-cleanup.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
- `test-container-privileged.sh` → `test/e2e/scenarios/container-execution.test.js`
|
||||
|
||||
- `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`
|
||||
**Security Tests** (Directory: `test/security/` - removed)
|
||||
|
||||
### Integration Tests
|
||||
- `test-firewall.sh` → `test/e2e/scenarios/security-firewall.test.js`
|
||||
- `test-github-token.sh` → `test/e2e/scenarios/github-integration.test.js`
|
||||
- `test-with-auth.sh` → `test/e2e/scenarios/security-firewall.test.js`
|
||||
|
||||
- `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`
|
||||
**Integration Tests** (Directory: `test/integration/` - removed)
|
||||
|
||||
- `test-full-flow.sh` → `test/e2e/scenarios/full-workflow.test.js`
|
||||
- `test-claudecode-docker.sh` → `test/e2e/scenarios/docker-execution.test.js` and `full-workflow.test.js`
|
||||
|
||||
### Retained Shell Scripts
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Debug script to log detailed information about check_suite webhooks
|
||||
* This helps diagnose why PR reviews might not be triggering
|
||||
*/
|
||||
|
||||
// Set required environment variables
|
||||
process.env.BOT_USERNAME = process.env.BOT_USERNAME || '@TestBot';
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || 'test-secret';
|
||||
process.env.GITHUB_TOKEN = process.env.GITHUB_TOKEN || 'test-token';
|
||||
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const { createLogger } = require('../src/utils/logger');
|
||||
|
||||
const logger = createLogger('debug-check-suite');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3333;
|
||||
|
||||
// Middleware to capture raw body for signature verification
|
||||
app.use(bodyParser.raw({ type: 'application/json' }));
|
||||
app.use((req, res, next) => {
|
||||
req.rawBody = req.body;
|
||||
req.body = JSON.parse(req.body.toString());
|
||||
next();
|
||||
});
|
||||
|
||||
// Debug webhook endpoint
|
||||
app.post('/webhook', (req, res) => {
|
||||
const event = req.headers['x-github-event'];
|
||||
const delivery = req.headers['x-github-delivery'];
|
||||
|
||||
logger.info(
|
||||
{
|
||||
event,
|
||||
delivery,
|
||||
headers: req.headers
|
||||
},
|
||||
'Received webhook'
|
||||
);
|
||||
|
||||
if (event === 'check_suite') {
|
||||
const payload = req.body;
|
||||
const checkSuite = payload.check_suite;
|
||||
const repo = payload.repository;
|
||||
|
||||
logger.info(
|
||||
{
|
||||
action: payload.action,
|
||||
repo: repo?.full_name,
|
||||
checkSuite: {
|
||||
id: checkSuite?.id,
|
||||
conclusion: checkSuite?.conclusion,
|
||||
status: checkSuite?.status,
|
||||
head_branch: checkSuite?.head_branch,
|
||||
head_sha: checkSuite?.head_sha,
|
||||
before: checkSuite?.before,
|
||||
after: checkSuite?.after,
|
||||
pull_requests_count: checkSuite?.pull_requests?.length || 0,
|
||||
pull_requests: checkSuite?.pull_requests?.map(pr => ({
|
||||
number: pr.number,
|
||||
id: pr.id,
|
||||
url: pr.url,
|
||||
head: pr.head,
|
||||
base: pr.base
|
||||
}))
|
||||
}
|
||||
},
|
||||
'CHECK_SUITE webhook details'
|
||||
);
|
||||
|
||||
// Log the full payload for deep inspection
|
||||
logger.debug(
|
||||
{
|
||||
fullPayload: JSON.stringify(payload, null, 2)
|
||||
},
|
||||
'Full webhook payload'
|
||||
);
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Webhook logged' });
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
logger.info({ port: PORT }, `Debug webhook server listening on port ${PORT}`);
|
||||
console.log('\nTo test this webhook receiver:');
|
||||
console.log(`1. Configure your GitHub webhook to point to: http://YOUR_SERVER:${PORT}/webhook`);
|
||||
console.log('2. Make sure to include check_suite events in the webhook configuration');
|
||||
console.log('3. Trigger a check suite completion in your repository');
|
||||
console.log('4. Check the logs above for detailed information\n');
|
||||
});
|
||||
@@ -17,14 +17,14 @@ conditionalDescribe(
|
||||
echo "Claude API test complete"
|
||||
`,
|
||||
env: {
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub',
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub',
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || 'test-key'
|
||||
}
|
||||
});
|
||||
|
||||
assertCommandSuccess(result);
|
||||
expect(result.stdout).toContain('Claude API test complete');
|
||||
expect(result.stdout).toContain('Repository: intelligence-assist/claude-hub');
|
||||
expect(result.stdout).toContain('Repository: claude-did-this/claude-hub');
|
||||
});
|
||||
|
||||
test('should validate Claude API environment setup', async () => {
|
||||
@@ -53,7 +53,7 @@ conditionalDescribe(
|
||||
const result = await containerExecutor.exec({
|
||||
entrypoint: '/bin/bash',
|
||||
command: 'echo "Container API test"',
|
||||
repo: 'intelligence-assist/test-repo',
|
||||
repo: 'claude-did-this/test-repo',
|
||||
env: {
|
||||
CONTAINER_MODE: 'api-test',
|
||||
API_ENDPOINT: 'test-endpoint'
|
||||
|
||||
@@ -107,7 +107,7 @@ conditionalDescribe(
|
||||
const result = await containerExecutor.exec({
|
||||
entrypoint: '/bin/bash',
|
||||
command: 'echo "Repository configuration test"',
|
||||
repo: 'intelligence-assist/test-repo',
|
||||
repo: 'claude-did-this/test-repo',
|
||||
env: {
|
||||
ISSUE_NUMBER: '42',
|
||||
IS_PULL_REQUEST: 'true'
|
||||
|
||||
@@ -19,7 +19,7 @@ conditionalDescribe(
|
||||
test('should handle complete environment setup', async () => {
|
||||
const result = await containerExecutor.execFullFlow({
|
||||
env: {
|
||||
TEST_REPO_FULL_NAME: 'intelligence-assist/test-repo',
|
||||
TEST_REPO_FULL_NAME: 'claude-did-this/test-repo',
|
||||
COMMAND: 'echo "Full workflow test"'
|
||||
}
|
||||
});
|
||||
@@ -34,7 +34,7 @@ conditionalDescribe(
|
||||
const result = await containerExecutor.exec({
|
||||
interactive: true,
|
||||
env: {
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub',
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub',
|
||||
ISSUE_NUMBER: '1',
|
||||
IS_PULL_REQUEST: 'false',
|
||||
COMMAND: 'echo "Claude Code Docker test"',
|
||||
@@ -60,7 +60,7 @@ conditionalDescribe(
|
||||
echo "Environment validation complete"
|
||||
`,
|
||||
env: {
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub',
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub',
|
||||
ISSUE_NUMBER: '42',
|
||||
COMMAND: 'validate environment'
|
||||
}
|
||||
@@ -76,7 +76,7 @@ conditionalDescribe(
|
||||
const result = await containerExecutor.exec({
|
||||
interactive: true,
|
||||
env: {
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub',
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub',
|
||||
ISSUE_NUMBER: '1',
|
||||
IS_PULL_REQUEST: 'false',
|
||||
COMMAND: 'echo "Integration test complete"',
|
||||
@@ -96,7 +96,7 @@ conditionalDescribe(
|
||||
interactive: true,
|
||||
volumes: [`${homeDir}/.aws:/home/node/.aws:ro`],
|
||||
env: {
|
||||
REPO_FULL_NAME: 'intelligence-assist/test-bedrock',
|
||||
REPO_FULL_NAME: 'claude-did-this/test-bedrock',
|
||||
ISSUE_NUMBER: '1',
|
||||
IS_PULL_REQUEST: 'false',
|
||||
COMMAND: 'echo "Bedrock integration test"',
|
||||
|
||||
@@ -70,7 +70,7 @@ conditionalDescribe(
|
||||
`,
|
||||
env: {
|
||||
GITHUB_TOKEN: process.env.GITHUB_TOKEN || 'test-token',
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub'
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub'
|
||||
},
|
||||
timeout: 15000
|
||||
});
|
||||
@@ -117,7 +117,7 @@ conditionalDescribe(
|
||||
`,
|
||||
env: {
|
||||
GITHUB_TOKEN: process.env.GITHUB_TOKEN || 'test-token',
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub',
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub',
|
||||
ISSUE_NUMBER: '1'
|
||||
},
|
||||
timeout: 15000
|
||||
@@ -166,7 +166,7 @@ conditionalDescribe(
|
||||
`,
|
||||
env: {
|
||||
GITHUB_TOKEN: process.env.GITHUB_TOKEN || 'test-token',
|
||||
REPO_FULL_NAME: 'intelligence-assist/claude-hub',
|
||||
REPO_FULL_NAME: 'claude-did-this/claude-hub',
|
||||
ISSUE_NUMBER: '1',
|
||||
IS_PULL_REQUEST: 'false'
|
||||
},
|
||||
|
||||
@@ -202,7 +202,7 @@ class ContainerExecutor {
|
||||
return this.exec({
|
||||
entrypoint: '/bin/bash',
|
||||
command:
|
||||
'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',
|
||||
"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",
|
||||
volumes: [`${homeDir}/.aws:/home/node/.aws:ro`],
|
||||
...options
|
||||
});
|
||||
|
||||
@@ -100,7 +100,6 @@ function conditionalDescribe(suiteName, suiteFunction, options = {}) {
|
||||
console.warn(
|
||||
`⚠️ Skipping test suite '${suiteName}': Missing environment variables: ${missing.join(', ')}`
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Test setup file to ensure required environment variables are set
|
||||
process.env.BOT_USERNAME = process.env.BOT_USERNAME || '@TestBot';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
@@ -154,7 +154,8 @@ describe('GitHub Controller - Check Suite Events', () => {
|
||||
issueNumber: 42,
|
||||
command: expect.stringContaining('# GitHub PR Review - Complete Automated Review'),
|
||||
isPullRequest: true,
|
||||
branchName: 'feature-branch'
|
||||
branchName: 'feature-branch',
|
||||
operationType: 'pr-review'
|
||||
});
|
||||
|
||||
// Verify simple success response
|
||||
@@ -426,7 +427,10 @@ describe('GitHub Controller - Check Suite Events', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should skip PR review when combined status is not success', async () => {
|
||||
it('should skip PR review when not all check suites are complete', async () => {
|
||||
// Use wait for all checks mode for this test
|
||||
process.env.PR_REVIEW_WAIT_FOR_ALL_CHECKS = 'true';
|
||||
|
||||
// Setup successful check suite with pull requests
|
||||
mockReq.body = {
|
||||
action: 'completed',
|
||||
@@ -459,45 +463,43 @@ describe('GitHub Controller - Check Suite Events', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Mock combined status to return pending
|
||||
githubService.getCombinedStatus.mockResolvedValue({
|
||||
state: 'pending',
|
||||
total_count: 5,
|
||||
statuses: [
|
||||
{ context: 'build', state: 'success', description: 'Build passed' },
|
||||
{ context: 'tests', state: 'pending', description: 'Tests running' }
|
||||
// Mock that some check suites are still in progress
|
||||
githubService.getCheckSuitesForRef.mockResolvedValue({
|
||||
check_suites: [
|
||||
{
|
||||
id: 12345,
|
||||
app: { name: 'GitHub Actions' },
|
||||
status: 'completed',
|
||||
conclusion: 'success'
|
||||
},
|
||||
{
|
||||
id: 12346,
|
||||
app: { name: 'CodeQL' },
|
||||
status: 'in_progress',
|
||||
conclusion: null
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await githubController.handleWebhook(mockReq, mockRes);
|
||||
|
||||
// Verify combined status was checked
|
||||
expect(githubService.getCombinedStatus).toHaveBeenCalled();
|
||||
// Verify check suites were queried
|
||||
expect(githubService.getCheckSuitesForRef).toHaveBeenCalled();
|
||||
|
||||
// Verify Claude was NOT called
|
||||
// Verify Claude was NOT called because not all checks are complete
|
||||
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
||||
|
||||
// Verify response indicates PR was skipped
|
||||
// Verify simple success response
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Check suite processed: 0 reviewed, 0 failed, 1 skipped',
|
||||
context: {
|
||||
repo: 'owner/repo',
|
||||
checkSuite: 12345,
|
||||
conclusion: 'success',
|
||||
results: [
|
||||
{
|
||||
prNumber: 42,
|
||||
success: false,
|
||||
error: null,
|
||||
skippedReason: 'Combined status is pending'
|
||||
}
|
||||
]
|
||||
}
|
||||
message: 'Webhook processed successfully'
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should handle combined status API errors', async () => {
|
||||
it('should handle check suites API errors gracefully', async () => {
|
||||
// Use wait for all checks mode for this test
|
||||
process.env.PR_REVIEW_WAIT_FOR_ALL_CHECKS = 'true';
|
||||
|
||||
// Setup successful check suite with pull requests
|
||||
mockReq.body = {
|
||||
action: 'completed',
|
||||
@@ -530,35 +532,25 @@ describe('GitHub Controller - Check Suite Events', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Mock combined status to throw error
|
||||
githubService.getCombinedStatus.mockRejectedValue(new Error('GitHub API error'));
|
||||
// Mock getCheckSuitesForRef to throw error
|
||||
githubService.getCheckSuitesForRef.mockRejectedValue(new Error('GitHub API error'));
|
||||
|
||||
await githubController.handleWebhook(mockReq, mockRes);
|
||||
|
||||
// Verify Claude was NOT called
|
||||
// Verify Claude was NOT called due to API error
|
||||
expect(claudeService.processCommand).not.toHaveBeenCalled();
|
||||
|
||||
// Verify response indicates failure
|
||||
// Verify simple success response (webhook processing succeeded even if check suites query failed)
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: 'Check suite processed: 0 reviewed, 0 failed, 1 skipped',
|
||||
context: {
|
||||
repo: 'owner/repo',
|
||||
checkSuite: 12345,
|
||||
conclusion: 'success',
|
||||
results: [
|
||||
{
|
||||
prNumber: 42,
|
||||
success: false,
|
||||
error: 'Failed to check status: GitHub API error',
|
||||
skippedReason: 'Status check failed'
|
||||
}
|
||||
]
|
||||
}
|
||||
message: 'Webhook processed successfully'
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip PR review when already reviewed at same commit', async () => {
|
||||
// Use specific workflow trigger for this test
|
||||
process.env.PR_REVIEW_WAIT_FOR_ALL_CHECKS = 'false';
|
||||
|
||||
// Setup successful check suite with pull request
|
||||
mockReq.body = {
|
||||
action: 'completed',
|
||||
@@ -591,6 +583,11 @@ describe('GitHub Controller - Check Suite Events', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Mock workflow name extraction to match PR_REVIEW_TRIGGER_WORKFLOW
|
||||
githubService.getCheckSuitesForRef.mockResolvedValue({
|
||||
check_runs: [{ name: 'Pull Request CI' }]
|
||||
});
|
||||
|
||||
// Mock that PR has already been reviewed at this commit
|
||||
githubService.hasReviewedPRAtCommit.mockResolvedValue(true);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('GitHub Controller - Webhook Validation', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
|
||||
mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis()
|
||||
@@ -372,4 +372,4 @@ describe('GitHub Controller - Webhook Validation', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Express App Error Handling', () => {
|
||||
},
|
||||
'Request error'
|
||||
);
|
||||
|
||||
|
||||
// Handle JSON parsing errors
|
||||
if (err instanceof SyntaxError && 'body' in err) {
|
||||
res.status(400).json({ error: 'Invalid JSON' });
|
||||
|
||||
@@ -52,7 +52,7 @@ jest.mock('../../src/utils/secureCredentials', () => ({
|
||||
|
||||
jest.mock('util', () => ({
|
||||
...jest.requireActual('util'),
|
||||
promisify: jest.fn((fn) => fn ? async (...args: any[]) => fn(...args) : fn)
|
||||
promisify: jest.fn(fn => (fn ? (...args: any[]) => fn(...args) : fn))
|
||||
}));
|
||||
|
||||
// Mock the entire claudeService to avoid complex dependency issues
|
||||
@@ -82,7 +82,7 @@ describe('Express Application', () => {
|
||||
jest.resetModules(); // Clear module cache to ensure fresh imports
|
||||
process.env = { ...originalEnv };
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
|
||||
// Reset mockExecSync to default behavior
|
||||
mockExecSync.mockImplementation(() => Buffer.from(''));
|
||||
});
|
||||
@@ -100,7 +100,7 @@ describe('Express Application', () => {
|
||||
describe('Application Structure', () => {
|
||||
it('should initialize Express app without starting server in test mode', () => {
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
expect(typeof app).toBe('function'); // Express app is a function
|
||||
expect(mockStartupMetrics.recordMilestone).toHaveBeenCalledWith(
|
||||
@@ -115,7 +115,7 @@ describe('Express Application', () => {
|
||||
|
||||
it('should record startup milestones during initialization', () => {
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
expect(mockStartupMetrics.recordMilestone).toHaveBeenCalledWith(
|
||||
'env_loaded',
|
||||
@@ -138,7 +138,7 @@ describe('Express Application', () => {
|
||||
it('should use correct port default when PORT is not set', () => {
|
||||
delete process.env.PORT;
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
// In test mode, the app is initialized but server doesn't start
|
||||
// so we can't directly test the port but we can verify app creation
|
||||
@@ -147,7 +147,7 @@ describe('Express Application', () => {
|
||||
it('should configure trust proxy when TRUST_PROXY is true', () => {
|
||||
process.env.TRUST_PROXY = 'true';
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
// Check that the trust proxy setting is configured
|
||||
expect(app.get('trust proxy')).toBe(true);
|
||||
@@ -156,7 +156,7 @@ describe('Express Application', () => {
|
||||
it('should not configure trust proxy when TRUST_PROXY is not set', () => {
|
||||
delete process.env.TRUST_PROXY;
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
// Trust proxy should not be set
|
||||
expect(app.get('trust proxy')).toBeFalsy();
|
||||
@@ -267,10 +267,10 @@ describe('Express Application', () => {
|
||||
milestones: {},
|
||||
startTime: Date.now() - 1000
|
||||
});
|
||||
|
||||
|
||||
const app = getApp();
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
// In CI, req.startupMetrics might be undefined due to middleware mocking
|
||||
// Just verify the response structure is correct
|
||||
@@ -284,7 +284,7 @@ describe('Express Application', () => {
|
||||
describe('Error Handling Middleware', () => {
|
||||
it('should handle JSON parsing errors', async () => {
|
||||
const app = getApp();
|
||||
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/github')
|
||||
.set('Content-Type', 'application/json')
|
||||
@@ -297,13 +297,13 @@ describe('Express Application', () => {
|
||||
it('should handle SyntaxError with body property', () => {
|
||||
const syntaxError = new SyntaxError('Unexpected token');
|
||||
(syntaxError as any).body = 'malformed';
|
||||
|
||||
|
||||
const mockReq = { method: 'POST', url: '/test' };
|
||||
const mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
|
||||
|
||||
// Test the error handler logic directly
|
||||
const errorHandler = (err: Error, req: any, res: any) => {
|
||||
if (err instanceof SyntaxError && 'body' in err) {
|
||||
@@ -312,9 +312,9 @@ describe('Express Application', () => {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
errorHandler(syntaxError, mockReq, mockRes);
|
||||
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid JSON' });
|
||||
});
|
||||
@@ -324,7 +324,7 @@ describe('Express Application', () => {
|
||||
it('should skip rate limiting in test environment', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
// Rate limiting is configured but should skip in test mode
|
||||
});
|
||||
@@ -332,7 +332,7 @@ describe('Express Application', () => {
|
||||
it('should apply rate limiting in non-test environment', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
const app = getApp();
|
||||
|
||||
|
||||
expect(app).toBeDefined();
|
||||
// Rate limiting should be active in production
|
||||
});
|
||||
@@ -341,7 +341,7 @@ describe('Express Application', () => {
|
||||
describe('Request Logging Middleware', () => {
|
||||
it('should log requests with response time', async () => {
|
||||
const app = getApp();
|
||||
|
||||
|
||||
await request(app).get('/health');
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
@@ -357,7 +357,7 @@ describe('Express Application', () => {
|
||||
|
||||
it('should sanitize method and url properly', async () => {
|
||||
const app = getApp();
|
||||
|
||||
|
||||
// Test that the logging middleware handles requests correctly
|
||||
await request(app).get('/health');
|
||||
|
||||
@@ -376,21 +376,21 @@ describe('Express Application', () => {
|
||||
describe('Body Parser Configuration', () => {
|
||||
it('should store raw body for webhook signature verification', async () => {
|
||||
const app = getApp();
|
||||
|
||||
|
||||
const testPayload = JSON.stringify({ test: 'data' });
|
||||
|
||||
|
||||
// Mock the routes to capture the req object
|
||||
let capturedReq: any = null;
|
||||
app.use('/test-body', (req: any, res: any) => {
|
||||
capturedReq = req;
|
||||
res.status(200).json({ success: true });
|
||||
});
|
||||
|
||||
|
||||
await request(app)
|
||||
.post('/test-body')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(testPayload);
|
||||
|
||||
|
||||
expect(capturedReq?.rawBody).toBeDefined();
|
||||
expect(capturedReq?.rawBody.toString()).toBe(testPayload);
|
||||
});
|
||||
@@ -402,14 +402,14 @@ describe('Express Application', () => {
|
||||
// (not as the main entry point), it doesn't start the server
|
||||
// The actual check is: if (require.main === module)
|
||||
const app = getApp();
|
||||
|
||||
|
||||
// Verify app exists but server wasn't started in test
|
||||
expect(app).toBeDefined();
|
||||
// In test mode, markReady should not be called since server doesn't start
|
||||
expect(mockStartupMetrics.markReady).not.toHaveBeenCalled();
|
||||
|
||||
|
||||
// Verify the app has the expected structure
|
||||
expect(typeof app).toBe('function'); // Express app is a function
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import request from 'supertest';
|
||||
import express from 'express';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import request from 'supertest';
|
||||
import express from 'express';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
@@ -98,13 +98,15 @@ describe('Claude Service - Docker Container Integration', () => {
|
||||
const testError = new Error('Claude API rate limit exceeded');
|
||||
processCommand.mockRejectedValueOnce(testError);
|
||||
|
||||
await expect(processCommand({
|
||||
repoFullName: 'owner/repo',
|
||||
issueNumber: 123,
|
||||
command: 'analyze repository',
|
||||
isPullRequest: false,
|
||||
branchName: null
|
||||
})).rejects.toThrow('Claude API rate limit exceeded');
|
||||
await expect(
|
||||
processCommand({
|
||||
repoFullName: 'owner/repo',
|
||||
issueNumber: 123,
|
||||
command: 'analyze repository',
|
||||
isPullRequest: false,
|
||||
branchName: null
|
||||
})
|
||||
).rejects.toThrow('Claude API rate limit exceeded');
|
||||
});
|
||||
|
||||
it('should handle network timeouts', async () => {
|
||||
@@ -112,13 +114,15 @@ describe('Claude Service - Docker Container Integration', () => {
|
||||
timeoutError.code = 'TIMEOUT';
|
||||
processCommand.mockRejectedValueOnce(timeoutError);
|
||||
|
||||
await expect(processCommand({
|
||||
repoFullName: 'owner/repo',
|
||||
issueNumber: 123,
|
||||
command: 'analyze large repository',
|
||||
isPullRequest: false,
|
||||
branchName: null
|
||||
})).rejects.toThrow('Request timeout');
|
||||
await expect(
|
||||
processCommand({
|
||||
repoFullName: 'owner/repo',
|
||||
issueNumber: 123,
|
||||
command: 'analyze large repository',
|
||||
isPullRequest: false,
|
||||
branchName: null
|
||||
})
|
||||
).rejects.toThrow('Request timeout');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,4 +155,4 @@ describe('Claude Service - Docker Container Integration', () => {
|
||||
expect(result).toContain('Repository access confirmed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('Claude Service', () => {
|
||||
});
|
||||
|
||||
// Verify test mode response
|
||||
expect(result).toContain('Hello! I\'m Claude responding to your request.');
|
||||
expect(result).toContain("Hello! I'm Claude responding to your request.");
|
||||
expect(result).toContain('test/repo');
|
||||
expect(sanitizeBotMentions).toHaveBeenCalled();
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ describe('githubService - Simple Coverage Tests', () => {
|
||||
it('should handle container keywords for docker', async () => {
|
||||
const labels = await githubService.getFallbackLabels(
|
||||
'Container startup issue',
|
||||
'The container won\'t start properly'
|
||||
"The container won't start properly"
|
||||
);
|
||||
|
||||
expect(labels).toContain('component:docker');
|
||||
|
||||
@@ -154,7 +154,7 @@ region = us-west-2
|
||||
process.env.AWS_PROFILE = 'non-existent-profile';
|
||||
|
||||
await expect(awsCredentialProvider.getCredentials()).rejects.toThrow(
|
||||
'Profile \'non-existent-profile\' not found'
|
||||
"Profile 'non-existent-profile' not found"
|
||||
);
|
||||
|
||||
// Restore AWS_PROFILE
|
||||
@@ -172,7 +172,7 @@ aws_access_key_id = test-access-key
|
||||
fsPromises.readFile.mockImplementationOnce(() => Promise.resolve(mockConfigFile));
|
||||
|
||||
await expect(awsCredentialProvider.getCredentials()).rejects.toThrow(
|
||||
'Incomplete credentials for profile \'test-profile\''
|
||||
"Incomplete credentials for profile 'test-profile'"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
// Mock the logger
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Test Runner Access
|
||||
|
||||
This file tests if our self-hosted runner is working properly.
|
||||
@@ -29,7 +29,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noErrorTruncation": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node", "jest"]
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
||||
Reference in New Issue
Block a user