From 7034d1593c2c7a8d425dcbaf67443a23fdaafba8 Mon Sep 17 00:00:00 2001 From: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:14:41 -0400 Subject: [PATCH] Add Docker support for Goose in CI/CD pipelines (#4434) Adds a production-ready Docker image for Goose that enables using Goose itself in CI pipelines to improve the codebase. --- .dockerignore | 103 +++++++++ .github/workflows/publish-docker.yml | 62 ++++++ .github/workflows/test-finder.yml | 143 ++++++++++++ BUILDING_DOCKER.md | 322 +++++++++++++++++++++++++++ Dockerfile | 73 ++++++ 5 files changed, 703 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/publish-docker.yml create mode 100644 .github/workflows/test-finder.yml create mode 100644 BUILDING_DOCKER.md create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..48f68357 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,103 @@ +# Build artifacts +target/ +**/target/ + +# Node modules and build outputs +node_modules/ +**/node_modules/ +ui/desktop/out/ +ui/desktop/dist/ +ui/desktop/src/bin/ + +# Git +.git/ +.gitignore + +# Documentation builds +documentation/build/ +documentation/.docusaurus/ +documentation/.cache-loader/ + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment and config files +.env +.env.* +*.log + +# CI/CD +.github/ +.hermit/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Test coverage +coverage/ +*.lcov + +# Python +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ + +# Rust analyzer +rust-project.json + +# Benchmarks +bench_results/ + +# Local configuration +.config/ +.local/ + +# Docker files (don't need to copy these into context) +Dockerfile +.dockerignore +docker-compose.yml + +# Development scripts that aren't needed in build +scripts/bench-postprocess-scripts/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +ehthumbs.db + +# Linux +.directory +.Trash-* + +# Archives +*.tar +*.tar.gz +*.tar.bz2 +*.zip +*.7z +*.rar + +# Backup files +*.bak +*.backup +*.old + +# Session files +*.jsonl +sessions/ + +# Generated files +ui/desktop/openapi.json +ui/desktop/src/api/ diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..4a8941c1 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,62 @@ +name: Publish Docker Image + +on: + push: + branches: + - main + tags: + - 'v*.*.*' + - 'v*.*.*-*' # For pre-releases like v1.2.3-beta + paths-ignore: + - 'documentation/**' + - '*.md' + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # pin@v3.11.1 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # pin@v3.5.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # pin@v5.8.0 + with: + images: ghcr.io/${{ github.repository_owner }}/goose + tags: | + # For main branch: latest, main, and sha + type=ref,event=branch + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + # For tags: v1.2.3 -> 1.2.3, 1.2, 1, latest (if not pre-release) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # For pre-release tags: keep the full version + type=raw,value={{tag}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and push Docker image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # pin@v6.18.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/test-finder.yml b/.github/workflows/test-finder.yml new file mode 100644 index 00000000..6cd8122e --- /dev/null +++ b/.github/workflows/test-finder.yml @@ -0,0 +1,143 @@ +name: Daily Test Coverage Finder + +on: + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (no PR creation)' + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + find-untested-code: + runs-on: ubuntu-latest + container: + image: ghcr.io/block/goose:latest + options: --user root + env: + GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'openai' }} + GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'gpt-4o' }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 + with: + fetch-depth: 0 + + - name: Install analysis tools + run: | + apt-get update + apt-get install -y jq ripgrep + + - name: Find untested code and create working test + id: find_untested + run: | + # Create analysis and test creation script + cat << 'EOF' > /tmp/create_working_test.txt + Your task is to find ONE untested function in the Rust codebase and create a working test for it. + + Requirements: + 1. The function MUST be in the crates/ directory + 2. It MUST have actual logic (not just a simple getter/setter) + 3. It MUST not already have a test + 4. Prefer functions with complexity but that are still testable in isolation + 5. Focus on the goose crate first, then goose-cli, then others + + Process: + 1. Find a suitable untested function + 2. Write a comprehensive unit test for it + 3. Apply your changes to the codebase + 4. Run the test with: cargo test --test + 5. If the test fails: + - Read and understand the error message + - Fix the test code + - Apply the fix and run the test again + - Repeat up to 3 times until the test passes + 6. Once the test passes (or after 3 attempts), save the final changes as a git diff to /tmp/test_addition.patch + 7. Report the outcome: + - If successful: write "SUCCESS" to /tmp/test_result.txt + - If no suitable function found: write "NO_FUNCTION_FOUND" to /tmp/test_result.txt + - If test couldn't be fixed: write "TEST_FAILED" to /tmp/test_result.txt + + Important: + - Only add ONE test for ONE function + - The test MUST compile and pass before creating the patch + - Keep changes minimal and focused + - Include a descriptive test name that explains what is being tested + EOF + + goose run -i /tmp/create_working_test.txt + + # Check the result + if [ -f /tmp/test_result.txt ]; then + RESULT=$(cat /tmp/test_result.txt) + echo "Test creation result: $RESULT" + + if [ "$RESULT" = "SUCCESS" ] && [ -f /tmp/test_addition.patch ]; then + echo "patch_created=true" >> $GITHUB_OUTPUT + # Extract function name from patch for PR title + FUNC_NAME=$(grep -E "fn test_|#\[test\]" /tmp/test_addition.patch | head -1 | sed 's/.*test_//' | sed 's/(.*//' || echo "function") + echo "function_name=${FUNC_NAME}" >> $GITHUB_OUTPUT + else + echo "patch_created=false" >> $GITHUB_OUTPUT + echo "Reason: $RESULT" + fi + else + echo "patch_created=false" >> $GITHUB_OUTPUT + echo "No result file created" + fi + + - name: Create Pull Request + if: steps.find_untested.outputs.patch_created == 'true' && github.event.inputs.dry_run != 'true' + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # pin@v7.0.8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "test: add test for ${{ steps.find_untested.outputs.function_name }}" + title: "test: add test coverage for ${{ steps.find_untested.outputs.function_name }}" + body: | + ## 🤖 Automated Test Addition + + This PR was automatically generated by Goose to improve test coverage. + + ### What changed? + Added a unit test for a previously untested function. + + ### Why? + Part of our daily automated test coverage improvement initiative. Goose analyzes the codebase to find untested but important functions and creates focused unit tests for them. + + ### Review checklist + - [ ] Test is meaningful and actually tests the function + - [ ] Test name is descriptive + - [ ] Test passes locally + - [ ] No unnecessary changes included + + --- + *Generated by the Daily Test Coverage Finder workflow* + branch: goose/test-coverage-${{ github.run_number }} + delete-branch: true + labels: | + goose-generated + test + automated + + - name: Summary + if: always() + env: + PATCH_CREATED: ${{ steps.find_untested.outputs.patch_created }} + FUNCTION_NAME: ${{ steps.find_untested.outputs.function_name }} + run: | + if [ "$PATCH_CREATED" == "true" ]; then + echo "✅ Successfully found untested code and created a test" + echo "📝 Function tested: $FUNCTION_NAME" + else + echo "â„šī¸ No suitable untested code found today" + fi diff --git a/BUILDING_DOCKER.md b/BUILDING_DOCKER.md new file mode 100644 index 00000000..260fdfed --- /dev/null +++ b/BUILDING_DOCKER.md @@ -0,0 +1,322 @@ +# Building and Running Goose with Docker + +This guide covers building Docker images for Goose CLI for production use, CI/CD pipelines, and local development. + +## Quick Start + +### Using Pre-built Images + +The easiest way to use Goose with Docker is to pull the pre-built image from GitHub Container Registry: + +```bash +# Pull the latest image +docker pull ghcr.io/block/goose:latest + +# Run Goose CLI +docker run --rm ghcr.io/block/goose:latest --version + +# Run with LLM configuration +docker run --rm \ + -e GOOSE_PROVIDER=openai \ + -e GOOSE_MODEL=gpt-4o \ + -e OPENAI_API_KEY=$OPENAI_API_KEY \ + ghcr.io/block/goose:latest run -t "Hello, world!" +``` + +## Building from Source + +### Prerequisites + +- Docker 20.10 or later +- Docker Buildx (for multi-platform builds) +- Git + +### Build the Image + +1. Clone the repository: +```bash +git clone https://github.com/block/goose.git +cd goose +``` + +2. Build the Docker image: +```bash +docker build -t goose:local . +``` + +The build process: +- Uses a multi-stage build to minimize final image size +- Compiles with optimizations (LTO, stripping, size optimization) +- Results in a ~340MB image containing the `goose` CLI binary + +### Build Options + +For a development build with debug symbols: +```bash +docker build --build-arg CARGO_PROFILE_RELEASE_STRIP=false -t goose:dev . +``` + +For multi-platform builds: +```bash +docker buildx build --platform linux/amd64,linux/arm64 -t goose:multi . +``` + +## Running Goose in Docker + +### CLI Mode + +Basic usage: +```bash +# Show help +docker run --rm goose:local --help + +# Run a command +docker run --rm \ + -e GOOSE_PROVIDER=openai \ + -e GOOSE_MODEL=gpt-4o \ + -e OPENAI_API_KEY=$OPENAI_API_KEY \ + goose:local run -t "Explain Docker containers" +``` + +With volume mounts for file access: +```bash +docker run --rm \ + -v $(pwd):/workspace \ + -w /workspace \ + -e GOOSE_PROVIDER=openai \ + -e GOOSE_MODEL=gpt-4o \ + -e OPENAI_API_KEY=$OPENAI_API_KEY \ + goose:local run -t "Analyze the code in this directory" +``` + +Interactive session mode with Databricks: +```bash +docker run -it --rm \ + -e GOOSE_PROVIDER=databricks \ + -e GOOSE_MODEL=databricks-dbrx-instruct \ + -e DATABRICKS_HOST="$DATABRICKS_HOST" \ + -e DATABRICKS_TOKEN="$DATABRICKS_TOKEN" \ + goose:local session +``` + + + +### Docker Compose + +Create a `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + goose: + image: ghcr.io/block/goose:latest + environment: + - GOOSE_PROVIDER=${GOOSE_PROVIDER:-openai} + - GOOSE_MODEL=${GOOSE_MODEL:-gpt-4o} + - OPENAI_API_KEY=${OPENAI_API_KEY} + volumes: + - ./workspace:/workspace + - goose-config:/home/goose/.config/goose + working_dir: /workspace + stdin_open: true + tty: true + +volumes: + goose-config: +``` + +Run with: +```bash +docker-compose run --rm goose session +``` + +## Configuration + +### Environment Variables + +The Docker image accepts all standard Goose environment variables: + +- `GOOSE_PROVIDER`: LLM provider (openai, anthropic, google, etc.) +- `GOOSE_MODEL`: Model to use (gpt-4o, claude-3-5-sonnet, etc.) +- Provider-specific API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) + +### Persistent Configuration + +Mount the configuration directory to persist settings: +```bash +docker run --rm \ + -v ~/.config/goose:/home/goose/.config/goose \ + goose:local configure +``` + +### Installing Additional Tools + +The image runs as a non-root user by default. To install additional packages: + +```bash +# Run as root to install packages +docker run --rm \ + -u root \ + --entrypoint bash \ + goose:local \ + -c "apt-get update && apt-get install -y vim && goose --version" + +# Or create a custom Dockerfile +FROM ghcr.io/block/goose:latest +USER root +RUN apt-get update && apt-get install -y \ + vim \ + tmux \ + && rm -rf /var/lib/apt/lists/* +USER goose +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +jobs: + analyze: + runs-on: ubuntu-latest + container: + image: ghcr.io/block/goose:latest + env: + GOOSE_PROVIDER: openai + GOOSE_MODEL: gpt-4o + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + steps: + - uses: actions/checkout@v4 + - name: Run Goose analysis + run: | + goose run -t "Review this codebase for security issues" +``` + +### GitLab CI + +```yaml +analyze: + image: ghcr.io/block/goose:latest + variables: + GOOSE_PROVIDER: openai + GOOSE_MODEL: gpt-4o + script: + - goose run -t "Generate documentation for this project" +``` + +## Image Details + +### Size and Optimization + +- **Base image**: Debian Bookworm Slim (minimal runtime dependencies) +- **Final size**: ~340MB +- **Optimizations**: Link-Time Optimization (LTO), binary stripping, size optimization +- **Binary included**: `/usr/local/bin/goose` (32MB) + +### Security + +- Runs as non-root user `goose` (UID 1000) +- Minimal attack surface with only essential runtime dependencies +- Regular security updates via automated builds + +### Included Tools + +The image includes essential tools for Goose operation: +- `git` - Version control operations +- `curl` - HTTP requests +- `ca-certificates` - SSL/TLS support +- Basic shell utilities + +## Troubleshooting + +### Permission Issues + +If you encounter permission errors when mounting volumes: +```bash +# Ensure the mounted directory is accessible +docker run --rm \ + -v $(pwd):/workspace \ + -u $(id -u):$(id -g) \ + goose:local run -t "List files" +``` + +### API Key Issues + +If API keys aren't being recognized: +1. Ensure environment variables are properly set +2. Check that quotes are handled correctly in your shell +3. Use `docker run --env-file .env` for multiple environment variables + +### Network Issues + +For accessing local services from within the container: +```bash +# Use host network mode +docker run --rm --network host goose:local +``` + +## Advanced Usage + +### Custom Entrypoint + +Override the default entrypoint for debugging: +```bash +docker run --rm -it --entrypoint bash goose:local +``` + +### Resource Limits + +Set memory and CPU limits: +```bash +docker run --rm \ + --memory="2g" \ + --cpus="2" \ + goose:local +``` + +### Multi-stage Development + +For development with hot reload: +```bash +# Mount source code +docker run --rm \ + -v $(pwd):/usr/src/goose \ + -w /usr/src/goose \ + rust:1.82-bookworm \ + cargo watch -x run +``` + +## Building for Production + +For production deployments: + +1. Use specific image tags instead of `latest` +2. Use secrets management for API keys +3. Set up logging and monitoring +4. Configure resource limits and auto-scaling + +Example production Dockerfile: +```dockerfile +FROM ghcr.io/block/goose:v1.6.0 +# Add any additional tools needed for your use case +USER root +RUN apt-get update && apt-get install -y your-tools && rm -rf /var/lib/apt/lists/* +USER goose +``` + +## Contributing + +When contributing Docker-related changes: + +1. Test builds on multiple platforms (amd64, arm64) +2. Verify image size remains reasonable +3. Update this documentation +4. Consider CI/CD implications +5. Test with various LLM providers + +## Related Documentation + +- [Goose in Docker Tutorial](documentation/docs/tutorials/goose-in-docker.md) - Step-by-step tutorial +- [Installation Guide](https://block.github.io/goose/docs/getting-started/installation) - All installation methods +- [Configuration Guide](https://block.github.io/goose/docs/guides/config-file) - Detailed configuration options diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..20280463 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,73 @@ +# syntax=docker/dockerfile:1.4 +# Goose CLI and Server Docker Image +# Multi-stage build for minimal final image size + +# Build stage +FROM rust:1.82-bookworm AS builder + +# Install build dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libssl-dev \ + libdbus-1-dev \ + protobuf-compiler \ + libprotobuf-dev \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Create app directory +WORKDIR /build + +# Copy source code +COPY . . + +# Build release binaries with optimizations +ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse +ENV CARGO_PROFILE_RELEASE_LTO=true +ENV CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 +ENV CARGO_PROFILE_RELEASE_OPT_LEVEL=z +ENV CARGO_PROFILE_RELEASE_STRIP=true +RUN cargo build --release --package goose-cli + +# Runtime stage - minimal Debian +FROM debian:bookworm-slim@sha256:b1a741487078b369e78119849663d7f1a5341ef2768798f7b7406c4240f86aef + +# Install only runtime dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + libssl3 \ + libdbus-1-3 \ + libxcb1 \ + curl \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy binary from builder +COPY --from=builder /build/target/release/goose /usr/local/bin/goose + +# Create non-root user +RUN useradd -m -u 1000 -s /bin/bash goose && \ + mkdir -p /home/goose/.config/goose && \ + chown -R goose:goose /home/goose + +# Set up environment +ENV PATH="/usr/local/bin:${PATH}" +ENV HOME="/home/goose" + +# Switch to non-root user +USER goose +WORKDIR /home/goose + +# Default to goose CLI +ENTRYPOINT ["/usr/local/bin/goose"] +CMD ["--help"] + +# Labels for metadata +LABEL org.opencontainers.image.title="Goose" +LABEL org.opencontainers.image.description="Goose CLI" +LABEL org.opencontainers.image.vendor="Block" +LABEL org.opencontainers.image.source="https://github.com/block/goose"