diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3e7b3b3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.env.local +config.json +*.pem +bauer +bauer-api +bauer-output/ +bauer-log.json +bauer-doc-suggestions.json +.git/ diff --git a/.env b/.env index 3625f18..54efc52 100644 --- a/.env +++ b/.env @@ -1,15 +1,12 @@ -# GitHub token for creating pull requests -# Generate at https://github.com/settings/tokens -GITHUB_TOKEN=ghp_xxxxxxxxxxxx - -# Service account email for domain-wide delegation (if useDelegation is enabled) -DELEGATION_EMAIL=your-service-account@your-project.iam.gserviceaccount.com - -# Optional: customize the target repository (defaults to canonical/ubuntu.com) -GITHUB_REPO_OWNER=canonical -GITHUB_REPO_NAME=ubuntu.com - -# Google Docs configuration -# Set the Google Doc URL to extract suggestions from -GOOGLE_DOC_URL=https://docs.google.com/document/d/your-document-id/edit - +# API Server +BAUER_API_PORT=8090 + +# Copilot defaults +BAUER_MODEL=gpt-5-mini-high +BAUER_SUMMARY_MODEL=gpt-5-mini-high + +# Bauer behavior defaults +BAUER_CHUNK_SIZE=1 +BAUER_PAGE_REFRESH=false +BAUER_OUTPUT_DIR=bauer-output +BAUER_BRANCH_PREFIX=bauer diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1faf987 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# --- Build stage --- +FROM golang:1.24-bookworm AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o bauer-api ./cmd/app/ + +# --- Runtime stage --- +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && apt-get update && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=builder /app/bauer-api . + +ENV BAUER_API_PORT=8090 +EXPOSE 8090 + +CMD ["./bauer-api"] diff --git a/Taskfile.yml b/Taskfile.yml index 358b4dd..6bd785a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -57,6 +57,21 @@ tasks: "https://api.figma.com/v1/files/{{.FILE_KEY}}/meta" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print('Name:', d.get('name','?'), '| Last modified:', d.get('lastModified','?'))" + docker-build: + desc: Build the Bauer API Docker image + cmds: + - docker build -t bauer-api:latest . + + docker-run: + desc: Run the Bauer API locally in Docker (reads .env.local for secrets) + cmds: + - | + docker run -p 8090:8090 \ + --env-file .env.local \ + -v "${BAUER_CREDENTIALS_PATH}:/creds/service-account.json:ro" \ + -e BAUER_CREDENTIALS_PATH=/creds/service-account.json \ + bauer-api:latest + clean: desc: Clean up generated files cmds: diff --git a/cmd/app/main.go b/cmd/app/main.go index b8cf8bd..db2a40c 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -13,6 +13,8 @@ import ( "log/slog" "net/http" "os" + + "github.com/joho/godotenv" ) func run() error { @@ -50,12 +52,18 @@ func run() error { Orchestrator: orch, } + port := os.Getenv("BAUER_API_PORT") + if port == "" { + port = "8090" + } + addr := ":" + port + mux := http.NewServeMux() mux.HandleFunc("/api/v1/job", v1.JobPost(rc)) mux.HandleFunc("/api/v1/health", v1.GetHealth) - mux.HandleFunc("/api/v1/workflow", workflow.ExecuteWorkflowHandler(orch)) - slog.Info("starting server", "address", ":8090") - err = http.ListenAndServe(":8090", middleware.RequestTrace(mux)) + mux.HandleFunc("POST /api/v1/workflows", workflow.ExecuteWorkflowHandler(orch)) + slog.Info("starting server", "address", addr) + err = http.ListenAndServe(addr, middleware.RequestTrace(mux)) if err != nil { slog.Error("server error", "error", err.Error()) @@ -66,6 +74,8 @@ func run() error { } func main() { + _ = godotenv.Load(".env") + _ = godotenv.Load(".env.local") if err := run(); err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) diff --git a/docs/implementation-log.md b/docs/implementation-log.md index d137886..3a6ba8c 100644 --- a/docs/implementation-log.md +++ b/docs/implementation-log.md @@ -268,9 +268,16 @@ _Parent: `feat/figma-phase-e-drift`_ **Tasks:** T3.0, T3.1, T3.2, T3.3, T3.4 -**Summary:** _(to be filled by agent)_ +**Summary:** Added Docker support, env-file loading, secrets removal from the API request body, route rename, and a build task. T3.0 introduced a multi-stage `Dockerfile` (golang:1.24 builder + debian:bookworm-slim runtime with git, curl, and the GitHub CLI installed) and a `.dockerignore` that excludes secrets, build artifacts, and the git directory; two new Taskfile tasks (`docker-build`, `docker-run`) wire the image build and local container run. T3.1 installed `github.com/joho/godotenv` and updated `cmd/app/main.go` to call `godotenv.Load` for both `.env` and `.env.local` (errors silently ignored) before calling `run()`; `.env` was replaced with a committed, non-sensitive defaults file covering port, model, chunk size, page-refresh, output directory, and branch prefix. T3.2 stripped `Credentials`, `GitHubToken`, `OutputDir`, and `LocalRepoPath` from `APIRequest`, replacing them with env-var lookups (`BAUER_CREDENTIALS_PATH`/`GOOGLE_APPLICATION_CREDENTIALS` and `github.GetGitHubToken()`) inside the handler; `firstNonEmpty` and `firstNonZero` helpers apply request-field-overrides-env semantics; `SummaryModel` was added to `APIRequest` for future use. T3.3 renamed the `/api/v1/workflow` route to `POST /api/v1/workflows` using Go 1.22 method+path routing. T3.4 was already present in the Taskfile from a prior branch (`build-api` task). -**Files changed:** _(to be filled by agent)_ +**Files changed:** +- `Dockerfile`: New multi-stage build — golang:1.24 builder compiles `bauer-api`; debian:bookworm-slim runtime installs git, curl, ca-certificates, and the GitHub CLI; exposes port 8090. +- `.dockerignore`: Excludes `.env.local`, `config.json`, `*.pem`, build binaries, `bauer-output/`, logs, and `.git/` from the Docker build context. +- `Taskfile.yml`: Added `docker-build` (builds `bauer-api:latest`) and `docker-run` (runs container with `--env-file .env.local` and a read-only credentials volume mount) tasks. +- `.env`: Replaced old placeholder content with committed non-sensitive defaults (`BAUER_API_PORT`, `BAUER_MODEL`, `BAUER_SUMMARY_MODEL`, `BAUER_CHUNK_SIZE`, `BAUER_PAGE_REFRESH`, `BAUER_OUTPUT_DIR`, `BAUER_BRANCH_PREFIX`). +- `cmd/app/main.go`: Added `github.com/joho/godotenv` import; `main()` now calls `godotenv.Load(".env")` and `godotenv.Load(".env.local")` before `run()`; route changed from `"/api/v1/workflow"` to `"POST /api/v1/workflows"` using Go 1.22 method+path syntax. +- `internal/workflow/api.go`: Removed `GitHubToken`, `Credentials`, `OutputDir`, and `LocalRepoPath` fields from `APIRequest`; added `SummaryModel` field; handler now resolves GitHub token via `github.GetGitHubToken()` and credentials from `BAUER_CREDENTIALS_PATH`/`GOOGLE_APPLICATION_CREDENTIALS`; added `firstNonEmpty` and `firstNonZero` helper functions; added `os` and `bauer/internal/github` imports. +- `go.mod` / `go.sum`: Added `github.com/joho/godotenv v1.5.1` dependency. --- diff --git a/go.mod b/go.mod index 613e71e..5642b30 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.24.0 require ( github.com/github/copilot-sdk/go v0.1.15 github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 golang.org/x/oauth2 v0.33.0 google.golang.org/api v0.257.0 ) @@ -18,7 +20,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index 988c8be..a16412c 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAV github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/internal/workflow/api.go b/internal/workflow/api.go index 891eb6b..14965af 100644 --- a/internal/workflow/api.go +++ b/internal/workflow/api.go @@ -5,29 +5,27 @@ import ( "fmt" "log/slog" "net/http" + "os" + "strconv" "time" + "bauer/internal/github" "bauer/internal/orchestrator" ) // APIRequest represents the API request for executing a workflow type APIRequest struct { // GitHub configuration - GitHubRepo string `json:"github_repo" binding:"required"` // "owner/repo" or HTTPS URL - GitHubToken string `json:"github_token" binding:"required"` // Personal access token - BranchPrefix string `json:"branch_prefix" default:"bauer"` // Branch naming prefix + GitHubRepo string `json:"github_repo"` // "owner/repo" or HTTPS URL + BranchPrefix string `json:"branch_prefix,omitempty"` // Branch naming prefix // Bauer configuration - DocID string `json:"doc_id" binding:"required"` // Google Doc ID - Credentials string `json:"credentials" binding:"required"` // Path to service account JSON - ChunkSize int `json:"chunk_size" default:"1"` // Number of chunks - PageRefresh bool `json:"page_refresh" default:"false"` // Page refresh mode - OutputDir string `json:"output_dir" default:"bauer-output"` // Output directory - Model string `json:"model" default:"gpt-5-mini-high"` // Copilot model - DryRun bool `json:"dry_run" default:"false"` // Dry run mode - - // Local repository path - LocalRepoPath string `json:"local_repo_path" default:"/tmp"` // Where to clone (optional) + DocID string `json:"doc_id"` // Google Doc ID + ChunkSize int `json:"chunk_size,omitempty"` // Number of chunks + PageRefresh bool `json:"page_refresh,omitempty"` // Page refresh mode + Model string `json:"model,omitempty"` // Copilot model + SummaryModel string `json:"summary_model,omitempty"` // Copilot summary model + DryRun bool `json:"dry_run,omitempty"` // Dry run mode } // APIResponse represents the API response from workflow execution @@ -62,49 +60,37 @@ func ExecuteWorkflowHandler(orch orchestrator.Orchestrator) http.HandlerFunc { writeError(w, http.StatusBadRequest, "github_repo is required") return } - if req.GitHubToken == "" { - writeError(w, http.StatusBadRequest, "github_token is required") - return - } if req.DocID == "" { writeError(w, http.StatusBadRequest, "doc_id is required") return } - if req.Credentials == "" { - writeError(w, http.StatusBadRequest, "credentials is required") - return - } - // Set defaults - if req.BranchPrefix == "" { - req.BranchPrefix = "bauer" - } - if req.LocalRepoPath == "" { - req.LocalRepoPath = "/tmp" - } - if req.OutputDir == "" { - req.OutputDir = "bauer-output" - } - if req.Model == "" { - req.Model = "gpt-5-mini-high" + // Resolve secrets from environment + token, err := github.GetGitHubToken() + if err != nil { + writeError(w, http.StatusInternalServerError, "no GitHub token configured: "+err.Error()) + return } - if req.ChunkSize == 0 { - req.ChunkSize = 1 + credentials := firstNonEmpty(os.Getenv("BAUER_CREDENTIALS_PATH"), os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + if credentials == "" { + writeError(w, http.StatusInternalServerError, "no credentials configured: set BAUER_CREDENTIALS_PATH or GOOGLE_APPLICATION_CREDENTIALS") + return } - // Create workflow input + // Create workflow input (request fields override env defaults) input := WorkflowInput{ GitHubRepo: req.GitHubRepo, - GitHubToken: req.GitHubToken, - BranchPrefix: req.BranchPrefix, + GitHubToken: token, + BranchPrefix: firstNonEmpty(req.BranchPrefix, os.Getenv("BAUER_BRANCH_PREFIX"), "bauer"), DocID: req.DocID, - Credentials: req.Credentials, - ChunkSize: req.ChunkSize, - PageRefresh: req.PageRefresh, - OutputDir: req.OutputDir, - Model: req.Model, + Credentials: credentials, + ChunkSize: firstNonZero(req.ChunkSize, envInt("BAUER_CHUNK_SIZE"), 1), + PageRefresh: req.PageRefresh || envBool("BAUER_PAGE_REFRESH"), + OutputDir: firstNonEmpty(os.Getenv("BAUER_OUTPUT_DIR"), "bauer-output"), + Model: firstNonEmpty(req.Model, os.Getenv("BAUER_MODEL"), "gpt-5-mini-high"), + SummaryModel: firstNonEmpty(req.SummaryModel, os.Getenv("BAUER_SUMMARY_MODEL"), "gpt-5-mini-high"), DryRun: req.DryRun, - LocalRepoPath: fmt.Sprintf("%s/%s-%d", req.LocalRepoPath, "bauer-workflow", time.Now().Unix()), + LocalRepoPath: fmt.Sprintf("%s/%s-%d", "/tmp", "bauer-workflow", time.Now().Unix()), } logger.Info("workflow API request", @@ -188,3 +174,31 @@ func writeError(w http.ResponseWriter, statusCode int, message string) { "timestamp": time.Now(), }) } + +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} + +func firstNonZero(vals ...int) int { + for _, v := range vals { + if v != 0 { + return v + } + } + return 0 +} + +func envInt(key string) int { + v, _ := strconv.Atoi(os.Getenv(key)) + return v +} + +func envBool(key string) bool { + v, _ := strconv.ParseBool(os.Getenv(key)) + return v +} diff --git a/internal/workflow/workflow.go b/internal/workflow/workflow.go index 3ae2b0b..ac4e8c2 100644 --- a/internal/workflow/workflow.go +++ b/internal/workflow/workflow.go @@ -26,8 +26,9 @@ type WorkflowInput struct { ChunkSize int PageRefresh bool OutputDir string - Model string - DryRun bool + Model string + SummaryModel string + DryRun bool // Local repository path LocalRepoPath string @@ -167,6 +168,7 @@ func ExecuteWorkflow(ctx context.Context, input WorkflowInput, orch orchestrator PageRefresh: config.BoolPtr(input.PageRefresh), OutputDir: input.OutputDir, Model: input.Model, + SummaryModel: input.SummaryModel, TargetRepo: ".", // Current directory is the cloned repo }