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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.env.local
config.json
*.pem
bauer
bauer-api
bauer-output/
bauer-log.json
bauer-doc-suggestions.json
.git/
Comment on lines +1 to +9
27 changes: 12 additions & 15 deletions .env
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
15 changes: 15 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +65 to +73

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docker-run task uses --env-file .env.local which makes vars available inside the container, but BAUER_CREDENTIALS_PATH in the -v mount is expanded by the host shell. The Taskfile header instructs users to set up env vars before running. Adding a dotenv directive would load secrets into all tasks (build, test, lint) which is not desirable. Current approach — documented env setup + --env-file for the container — is the right trade-off for a dev-only task.

Comment on lines +69 to +73

clean:
desc: Clean up generated files
cmds:
Expand Down
16 changes: 13 additions & 3 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"log/slog"
"net/http"
"os"

"github.com/joho/godotenv"
)

func run() error {
Expand Down Expand Up @@ -50,12 +52,18 @@ func run() error {
Orchestrator: orch,
}

port := os.Getenv("BAUER_API_PORT")
if port == "" {
port = "8090"
}
addr := ":" + port

Comment on lines +59 to +60
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))

Comment on lines +64 to 67
if err != nil {
slog.Error("server error", "error", err.Error())
Expand All @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions docs/implementation-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
102 changes: 58 additions & 44 deletions internal/workflow/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 22 to +27
DryRun bool `json:"dry_run,omitempty"` // Dry run mode
}

// APIResponse represents the API response from workflow execution
Expand Down Expand Up @@ -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")
Comment on lines 73 to +76
return
}
Comment on lines +74 to 78

// 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"),
Comment on lines 79 to +90
Comment on lines +86 to +90
SummaryModel: firstNonEmpty(req.SummaryModel, os.Getenv("BAUER_SUMMARY_MODEL"), "gpt-5-mini-high"),
DryRun: req.DryRun,
Comment on lines +80 to 92
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",
Expand Down Expand Up @@ -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
}
6 changes: 4 additions & 2 deletions internal/workflow/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down