Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.git
.github
.claude
.grepai
.vscode
.idea
.cursor
bin
dist
docs
node_modules
qdrant_storage
*.md
!go.mod
!go.sum
LICENSE
.goreleaser.yml
.golangci.yml
.gitignore
.cursorrules
coverage.out
coverage.html
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ updates:
- dependencies
- go

- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
commit-message:
prefix: "deps"
labels:
- dependencies
- docker

- package-ecosystem: github-actions
directory: /
schedule:
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Docker

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
pull_request:
paths:
- "Dockerfile"
- ".dockerignore"

permissions:
contents: read
packages: write

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Build test image
run: docker build -t grepai-test .

- name: Test - binary runs
run: docker run --rm --entrypoint /grepai grepai-test version

- name: Test - config generation with defaults
run: |
mkdir -p /tmp/grepai-test-default && sudo chown 65534:65534 /tmp/grepai-test-default
docker run --rm -v /tmp/grepai-test-default:/workspace grepai-test || true
sudo grep 'provider: ollama' /tmp/grepai-test-default/.grepai/config.yaml

- name: Test - config generation with env vars
run: |
mkdir -p /tmp/grepai-test-env && sudo chown 65534:65534 /tmp/grepai-test-env
docker run --rm \
-v /tmp/grepai-test-env:/workspace \
-e GREPAI_PROVIDER=openai \
-e GREPAI_MODEL=text-embedding-3-large \
-e GREPAI_API_KEY=sk-test \
-e GREPAI_BACKEND=postgres \
grepai-test || true
sudo grep 'provider: openai' /tmp/grepai-test-env/.grepai/config.yaml
sudo grep 'text-embedding-3-large' /tmp/grepai-test-env/.grepai/config.yaml

- name: Test - config not overwritten (idempotent)
run: |
mkdir -p /tmp/grepai-test-idem/.grepai
echo -e "version: 1\ncustom: true" > /tmp/grepai-test-idem/.grepai/config.yaml
sudo chown -R 65534:65534 /tmp/grepai-test-idem
docker run --rm \
-v /tmp/grepai-test-idem:/workspace \
-e GREPAI_PROVIDER=openai \
grepai-test || true
sudo grep 'custom: true' /tmp/grepai-test-idem/.grepai/config.yaml

- name: Test - image size < 50MB
run: |
SIZE=$(docker image inspect grepai-test --format='{{.Size}}')
echo "Image size: $((SIZE / 1024 / 1024))MB"
[ "$SIZE" -lt 52428800 ] || (echo "Image too large!" && exit 1)

build-and-push:
needs: [test]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/yoanbernabeu/grepai
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
build-args: VERSION=${{ steps.meta.outputs.version }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Docker Support**: Official Docker image published on GHCR with environment variable configuration for all providers and backends
- **Shell Completion**: New `grepai completion [zsh|bash|fish|powershell]` command for shell autocompletion (#175)
- Static completions with descriptions for `--provider`, `--backend`, `--mode` flags
- Dynamic completions for `--workspace` and `--project` flags (loaded from config)
Expand Down
26 changes: 26 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Stage 1 - Builder
FROM golang:1.24-alpine AS builder

ARG VERSION=dev

WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION}" -o /grepai ./cmd/grepai

# Stage 2 - Runtime
FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder /grepai /grepai

USER 65534

WORKDIR /workspace

ENTRYPOINT ["/grepai", "watch", "--no-ui", "--auto-init"]
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pre-commit: fmt
go test -race ./...
@echo "✓ All checks passed! Ready to commit."

# Docker: build image locally
docker-build:
docker build -t grepai .

# Nix flake: compute vendorHash via Docker
nix-hash:
@echo "Computing vendorHash (requires Docker)..."
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ curl -sSL https://raw.githubusercontent.com/yoanbernabeu/grepai/main/install.sh
irm https://raw.githubusercontent.com/yoanbernabeu/grepai/main/install.ps1 | iex
```

**Docker:**
```bash
docker run -v /path/to/project:/workspace \
-e GREPAI_PROVIDER=ollama \
-e GREPAI_ENDPOINT=http://host.docker.internal:11434 \
ghcr.io/yoanbernabeu/grepai
```

Requires an embedding provider — [Ollama](https://ollama.ai) (default), [LM Studio](https://lmstudio.ai), or OpenAI.

**Ollama (recommended):**
Expand Down
9 changes: 9 additions & 0 deletions cli/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var (
watchStop bool
watchWorkspace string
watchNoUI bool
watchAutoInit bool
)

var (
Expand Down Expand Up @@ -84,6 +85,7 @@ func init() {
watchCmd.Flags().BoolVar(&watchStop, "stop", false, "Stop the background watcher")
watchCmd.Flags().StringVar(&watchWorkspace, "workspace", "", "Workspace name for multi-project mode")
watchCmd.Flags().BoolVar(&watchNoUI, "no-ui", false, "Disable interactive UI in foreground mode")
watchCmd.Flags().BoolVar(&watchAutoInit, "auto-init", false, "Generate config from GREPAI_* env vars if not present")
}

func runWatch(cmd *cobra.Command, args []string) error {
Expand All @@ -102,6 +104,13 @@ func runWatch(cmd *cobra.Command, args []string) error {
return fmt.Errorf("flags --background, --status, and --stop are mutually exclusive")
}

// Auto-init: generate config from env vars if not present
if watchAutoInit {
if err := config.GenerateFromEnv("."); err != nil {
return fmt.Errorf("auto-init failed: %w", err)
}
}

// Determine log directory
logDir := watchLogDir
if logDir == "" {
Expand Down
12 changes: 12 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ services:
# # count: all
# # capabilities: [gpu]

# grepai watcher (indexes mounted code)
grepai:
image: ghcr.io/yoanbernabeu/grepai:latest
# build: . # Uncomment to build locally
environment:
- GREPAI_PROVIDER=ollama
- GREPAI_ENDPOINT=http://host.docker.internal:11434
volumes:
- ./:/workspace
profiles:
- watch

volumes:
grepai-pgdata:
# grepai-ollama:
105 changes: 105 additions & 0 deletions config/envconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package config

import (
"fmt"
"log"
"os"
"strconv"
)

// GenerateFromEnv generates a .grepai/config.yaml from GREPAI_* environment
// variables. It is idempotent: if the config file already exists, it does
// nothing. This replaces the shell-based docker-entrypoint.sh logic.
func GenerateFromEnv(projectRoot string) error {
if Exists(projectRoot) {
log.Printf("Config already exists at %s, skipping generation", GetConfigPath(projectRoot))
return nil
}

provider := envOrDefault("GREPAI_PROVIDER", DefaultEmbedderProvider)

// Start from provider defaults
cfg := DefaultConfig()
cfg.Embedder = DefaultEmbedderForProvider(provider)
cfg.Store = DefaultStoreForBackend(envOrDefault("GREPAI_BACKEND", "gob"))

// Override embedder fields from env vars
if v := os.Getenv("GREPAI_MODEL"); v != "" {
cfg.Embedder.Model = v
}
if v := os.Getenv("GREPAI_ENDPOINT"); v != "" {
cfg.Embedder.Endpoint = v
}
if v := os.Getenv("GREPAI_API_KEY"); v != "" {
cfg.Embedder.APIKey = v
}
if v := os.Getenv("GREPAI_DIMENSIONS"); v != "" {
dim, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid GREPAI_DIMENSIONS %q: %w", v, err)
}
cfg.Embedder.Dimensions = &dim
}
if v := os.Getenv("GREPAI_PARALLELISM"); v != "" {
p, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid GREPAI_PARALLELISM %q: %w", v, err)
}
cfg.Embedder.Parallelism = p
}

// Override store fields from env vars
if v := os.Getenv("GREPAI_POSTGRES_DSN"); v != "" {
cfg.Store.Postgres.DSN = v
}
if v := os.Getenv("GREPAI_QDRANT_ENDPOINT"); v != "" {
cfg.Store.Qdrant.Endpoint = v
}
if v := os.Getenv("GREPAI_QDRANT_PORT"); v != "" {
p, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid GREPAI_QDRANT_PORT %q: %w", v, err)
}
cfg.Store.Qdrant.Port = p
}
if v := os.Getenv("GREPAI_QDRANT_COLLECTION"); v != "" {
cfg.Store.Qdrant.Collection = v
}
if v := os.Getenv("GREPAI_QDRANT_API_KEY"); v != "" {
cfg.Store.Qdrant.APIKey = v
}
if v := os.Getenv("GREPAI_QDRANT_USE_TLS"); v != "" {
cfg.Store.Qdrant.UseTLS = v == "true" || v == "1"
}

// Override chunking fields from env vars
if v := os.Getenv("GREPAI_CHUNKING_SIZE"); v != "" {
s, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid GREPAI_CHUNKING_SIZE %q: %w", v, err)
}
cfg.Chunking.Size = s
}
if v := os.Getenv("GREPAI_CHUNKING_OVERLAP"); v != "" {
o, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid GREPAI_CHUNKING_OVERLAP %q: %w", v, err)
}
cfg.Chunking.Overlap = o
}

if err := cfg.Save(projectRoot); err != nil {
return fmt.Errorf("failed to save generated config: %w", err)
}

log.Printf("Config generated at %s (provider: %s, backend: %s)",
GetConfigPath(projectRoot), cfg.Embedder.Provider, cfg.Store.Backend)
return nil
}

func envOrDefault(key, defaultValue string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultValue
}
Loading
Loading