Skip to content

Commit fc3d80d

Browse files
author
jxu3
committed
Update Go version to 1.23, enhance Makefile for multi-OS builds, and improve authentication handling
- Updated Go version in CI and release workflows - Added Makefile targets for building binaries for different OS/architectures - Enhanced AuthService to support configurable token refresh and path for tests - Updated ProxyService to validate tokens and handle errors more effectively - Improved error handling in API requests and tests for better coverage
1 parent f373d83 commit fc3d80d

File tree

13 files changed

+277
-47
lines changed

13 files changed

+277
-47
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ jobs:
2020
- name: Set up Go
2121
uses: actions/setup-go@v4
2222
with:
23-
go-version: '1.21'
23+
go-version: '1.23'
24+
25+
- name: Diagnostic: Print Go version
26+
run: go version
2427

2528
- name: Cache Go modules
2629
uses: actions/cache@v3
@@ -62,7 +65,7 @@ jobs:
6265
- name: Set up Go
6366
uses: actions/setup-go@v4
6467
with:
65-
go-version: '1.21'
68+
go-version: '1.23'
6669

6770
- name: golangci-lint
6871
uses: golangci/golangci-lint-action@v3
@@ -79,7 +82,7 @@ jobs:
7982
- name: Set up Go
8083
uses: actions/setup-go@v4
8184
with:
82-
go-version: '1.21'
85+
go-version: '1.23'
8386

8487
- name: Run Go Security Checks
8588
run: |
@@ -99,7 +102,7 @@ jobs:
99102
- name: Set up Go
100103
uses: actions/setup-go@v4
101104
with:
102-
go-version: '1.21'
105+
go-version: '1.23'
103106

104107
- name: Build binary
105108
run: |

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Set up Go
2424
uses: actions/setup-go@v4
2525
with:
26-
go-version: '1.21'
26+
go-version: '1.23'
2727

2828
- name: Get next version
2929
id: version
@@ -91,7 +91,7 @@ jobs:
9191
- name: Set up Go
9292
uses: actions/setup-go@v4
9393
with:
94-
go-version: '1.21'
94+
go-version: '1.23'
9595

9696
- name: Build binary
9797
env:

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
FROM golang:1.21-alpine AS builder
1+
FROM golang:1.23-alpine AS builder
22

33
WORKDIR /app
44

55
# Install git and ca-certificates for building
66
RUN apk add --no-cache git ca-certificates
7+
# Diagnostic: Print Go version in build environment
8+
RUN go version
79

810
# Copy go mod and sum files
911
COPY go.mod go.sum ./

Makefile

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ VERSION?=dev
88
build:
99
go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY) $(CMD_PATH)
1010

11+
# Build for specific OS/ARCH
12+
build-linux-amd64:
13+
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)-linux-amd64 $(CMD_PATH)
14+
15+
build-linux-arm64:
16+
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)-linux-arm64 $(CMD_PATH)
17+
18+
build-darwin-amd64:
19+
GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)-darwin-amd64 $(CMD_PATH)
20+
21+
build-darwin-arm64:
22+
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)-darwin-arm64 $(CMD_PATH)
23+
24+
build-windows-amd64:
25+
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)-windows-amd64.exe $(CMD_PATH)
26+
27+
build-windows-arm64:
28+
GOOS=windows GOARCH=arm64 go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)-windows-arm64.exe $(CMD_PATH)
29+
1130
# Run the application
1231
run: build
1332
./$(BINARY) start
@@ -97,25 +116,31 @@ docker-run:
97116
# Help
98117
help:
99118
@echo "Available targets:"
100-
@echo " build Build the binary"
101-
@echo " run Build and run the application"
102-
@echo " dev Run development server with hot reload"
103-
@echo " test Run unit tests (default)"
104-
@echo " test-unit Run only unit tests"
105-
@echo " test-integration Run only integration tests"
106-
@echo " test-e2e Run only e2e tests"
107-
@echo " test-all Run all tests"
108-
@echo " test-coverage Run tests with coverage report"
109-
@echo " test-short Run tests (skip integration tests)"
110-
@echo " test-verbose Run tests with verbose output"
111-
@echo " lint Lint the code"
112-
@echo " fmt Format the code"
113-
@echo " vet Vet the code"
114-
@echo " clean Clean build artifacts and test cache"
115-
@echo " deps Install dependencies"
116-
@echo " update-deps Update dependencies"
117-
@echo " security Run security checks"
118-
@echo " mocks Generate mocks"
119-
@echo " docker-build Build Docker image"
120-
@echo " docker-run Run Docker container"
121-
@echo " help Show this help message"
119+
@echo " build Build the binary"
120+
@echo " build-linux-amd64 Build for Linux amd64"
121+
@echo " build-linux-arm64 Build for Linux arm64"
122+
@echo " build-darwin-amd64 Build for macOS amd64"
123+
@echo " build-darwin-arm64 Build for macOS arm64"
124+
@echo " build-windows-amd64 Build for Windows amd64"
125+
@echo " build-windows-arm64 Build for Windows arm64"
126+
@echo " run Build and run the application"
127+
@echo " dev Run development server with hot reload"
128+
@echo " test Run unit tests (default)"
129+
@echo " test-unit Run only unit tests"
130+
@echo " test-integration Run only integration tests"
131+
@echo " test-e2e Run only e2e tests"
132+
@echo " test-all Run all tests"
133+
@echo " test-coverage Run tests with coverage report"
134+
@echo " test-short Run tests (skip integration tests)"
135+
@echo " test-verbose Run tests with verbose output"
136+
@echo " lint Lint the code"
137+
@echo " fmt Format the code"
138+
@echo " vet Vet the code"
139+
@echo " clean Clean build artifacts and test cache"
140+
@echo " deps Install dependencies"
141+
@echo " update-deps Update dependencies"
142+
@echo " security Run security checks"
143+
@echo " mocks Generate mocks"
144+
@echo " docker-build Build Docker image"
145+
@echo " docker-run Run Docker container"
146+
@echo " help Show this help message"

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ make docker-build # Build Docker image
106106
make docker-run # Run Docker container
107107
```
108108

109+
## Building for Different OS/Architectures
110+
111+
You can build binaries for different platforms using the following Makefile targets:
112+
113+
- `make build-linux-amd64` Build for Linux amd64
114+
- `make build-linux-arm64` Build for Linux arm64
115+
- `make build-darwin-amd64` Build for macOS amd64
116+
- `make build-darwin-arm64` Build for macOS arm64
117+
- `make build-windows-amd64` Build for Windows amd64
118+
- `make build-windows-arm64` Build for Windows arm64
119+
120+
The output binaries will be named accordingly (e.g., `github-copilot-svcs-windows-arm64.exe`).
121+
109122
## Installation & Usage
110123

111124
### 1. Build the Application

internal/auth.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,37 @@ type copilotTokenResponse struct {
4747
// Service provides authentication operations
4848
type AuthService struct {
4949
httpClient *http.Client
50+
51+
// For testability: override config save path
52+
configPath string
53+
54+
// For testability: optional custom token refresh function
55+
refreshFunc func(cfg *Config) error
5056
}
5157

5258
// NewAuthService creates a new auth service
53-
func NewAuthService(httpClient *http.Client) *AuthService {
54-
return &AuthService{
59+
func NewAuthService(httpClient *http.Client, opts ...func(*AuthService)) *AuthService {
60+
svc := &AuthService{
5561
httpClient: httpClient,
5662
}
63+
for _, opt := range opts {
64+
opt(svc)
65+
}
66+
return svc
67+
}
68+
69+
// Option to set config path for tests
70+
func WithConfigPath(path string) func(*AuthService) {
71+
return func(s *AuthService) {
72+
s.configPath = path
73+
}
74+
}
75+
76+
// Option to set custom refresh function for tests
77+
func WithRefreshFunc(f func(cfg *Config) error) func(*AuthService) {
78+
return func(s *AuthService) {
79+
s.refreshFunc = f
80+
}
5781
}
5882

5983
// Authenticate performs the full GitHub Copilot authentication flow
@@ -95,8 +119,14 @@ func (s *AuthService) Authenticate(cfg *Config) error {
95119
cfg.ExpiresAt = expiresAt
96120
cfg.RefreshIn = refreshIn
97121

98-
if err := cfg.SaveConfig(); err != nil {
99-
return fmt.Errorf("failed to save config: %w", err)
122+
var saveErr error
123+
if s.configPath != "" {
124+
saveErr = cfg.SaveConfig(s.configPath)
125+
} else {
126+
saveErr = cfg.SaveConfig()
127+
}
128+
if saveErr != nil {
129+
return fmt.Errorf("failed to save config: %w", saveErr)
100130
}
101131

102132
fmt.Println("Authentication successful!")
@@ -109,6 +139,19 @@ func (s *AuthService) RefreshToken(cfg *Config) error {
109139
}
110140

111141
func (s *AuthService) RefreshTokenWithContext(ctx context.Context, cfg *Config) error {
142+
if s.refreshFunc != nil {
143+
// Use injected refresh function for tests
144+
err := s.refreshFunc(cfg)
145+
if err != nil {
146+
return err
147+
}
148+
// Save config to injected path if set
149+
if s.configPath != "" {
150+
return cfg.SaveConfig(s.configPath)
151+
}
152+
return cfg.SaveConfig()
153+
}
154+
112155
if cfg.GitHubToken == "" {
113156
Warn("Cannot refresh token: no GitHub token available")
114157
return NewAuthError("no GitHub token available for refresh", nil)

internal/config.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,16 @@ func (c *Config) validateCORS() error {
406406
}
407407

408408
// SaveConfig saves the configuration to file
409-
func (c *Config) SaveConfig() error {
410-
path, err := GetConfigPath()
411-
if err != nil {
412-
return err
409+
func (c *Config) SaveConfig(pathOverride ...string) error {
410+
var path string
411+
var err error
412+
if len(pathOverride) > 0 && pathOverride[0] != "" {
413+
path = pathOverride[0]
414+
} else {
415+
path, err = GetConfigPath()
416+
if err != nil {
417+
return err
418+
}
413419
}
414420
f, err := os.Create(path)
415421
if err != nil {

internal/logger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func NewLogger(level string) *Logger {
3535
Level: logLevel,
3636
}
3737

38-
handler := slog.NewJSONHandler(os.Stdout, opts)
38+
handler := slog.NewTextHandler(os.Stdout, opts)
3939
return &Logger{slog.New(handler)}
4040
}
4141

internal/proxy.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"crypto/sha256"
77
"encoding/hex"
8+
"encoding/json"
89
"fmt"
910
"io"
1011
"net/http"
@@ -71,6 +72,7 @@ type CoalescingCache struct {
7172
type ProxyService struct {
7273
config *Config
7374
httpClient *http.Client
75+
authService *AuthService
7476
workerPool WorkerPoolInterface
7577
circuitBreaker *CircuitBreaker
7678
bufferPool *sync.Pool
@@ -139,7 +141,7 @@ func (cc *CoalescingCache) CoalesceRequest(key string, fn func() interface{}) in
139141
}
140142

141143
// NewProxyService creates a new proxy service
142-
func NewProxyService(cfg *Config, httpClient *http.Client, workerPool WorkerPoolInterface) *ProxyService {
144+
func NewProxyService(cfg *Config, httpClient *http.Client, authService *AuthService, workerPool WorkerPoolInterface) *ProxyService {
143145
circuitBreaker := &CircuitBreaker{
144146
state: CircuitClosed,
145147
timeout: time.Duration(cfg.Timeouts.CircuitBreaker) * time.Second,
@@ -154,6 +156,7 @@ func NewProxyService(cfg *Config, httpClient *http.Client, workerPool WorkerPool
154156
return &ProxyService{
155157
config: cfg,
156158
httpClient: httpClient,
159+
authService: authService,
157160
workerPool: workerPool,
158161
circuitBreaker: circuitBreaker,
159162
bufferPool: bufferPool,
@@ -203,7 +206,18 @@ func (s *ProxyService) Handler() http.HandlerFunc {
203206
Error("Worker error", "error", err)
204207
// Only write error if headers haven't been sent
205208
if !respWrapper.headersSent {
206-
http.Error(w, err.Error(), http.StatusInternalServerError)
209+
switch {
210+
case strings.Contains(err.Error(), "authentication error"):
211+
http.Error(w, err.Error(), http.StatusUnauthorized)
212+
case strings.Contains(err.Error(), "token validation failed"):
213+
http.Error(w, err.Error(), http.StatusUnauthorized)
214+
case strings.Contains(err.Error(), "bad request"):
215+
http.Error(w, err.Error(), http.StatusBadRequest)
216+
case strings.Contains(err.Error(), "method not allowed"):
217+
http.Error(w, err.Error(), http.StatusMethodNotAllowed)
218+
default:
219+
http.Error(w, err.Error(), http.StatusInternalServerError)
220+
}
207221
}
208222
}
209223
case <-ctx.Done():
@@ -279,14 +293,40 @@ func (cb *CircuitBreaker) onFailure() {
279293
func (s *ProxyService) processProxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
280294
Debug("Starting proxy request", "method", r.Method, "path", r.URL.Path)
281295

296+
// Validate method
297+
if r.Method != http.MethodPost {
298+
return fmt.Errorf("method not allowed: %s", r.Method)
299+
}
300+
282301
// Read the request body
283302
body, err := io.ReadAll(r.Body)
284303
if err != nil {
285304
Error("Error reading request body", "error", err)
286-
return NewProxyError("read_request_body", "failed to read request body", err)
305+
// Check for "http: request body too large" error and return 413
306+
if strings.Contains(err.Error(), "http: request body too large") {
307+
return fmt.Errorf("payload too large: %w", err)
308+
}
309+
return fmt.Errorf("bad request: failed to read request body: %w", err)
287310
}
288311
defer r.Body.Close()
289312

313+
// Basic body validation (for demonstration: consider empty body an error)
314+
if len(body) == 0 {
315+
return fmt.Errorf("bad request: empty request body")
316+
}
317+
318+
// Strict JSON validation before authentication
319+
var js json.RawMessage
320+
if jsonErr := json.Unmarshal(body, &js); jsonErr != nil {
321+
return fmt.Errorf("bad request: invalid JSON: %w", jsonErr)
322+
}
323+
324+
// Ensure we have a valid token before making the request
325+
if tokenErr := s.authService.EnsureValidToken(s.config); tokenErr != nil {
326+
Error("Failed to ensure valid token", "error", tokenErr)
327+
return NewAuthError("token validation failed", tokenErr)
328+
}
329+
290330
// Create new request to GitHub Copilot
291331
targetURL := copilotAPIBase + chatCompletionsPath
292332
Debug("Sending request to target", "url", targetURL, "body_length", len(body))

internal/server.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,15 @@ func CreateHTTPClient(cfg *Config) *http.Client {
104104
func NewServer(cfg *Config, httpClient *http.Client) *Server {
105105
workerPool := NewWorkerPool(runtime.NumCPU() * workerMultiplier)
106106

107+
// Create auth service
108+
authService := NewAuthService(httpClient)
109+
107110
// Create coalescing cache for models
108111
coalescingCache := NewCoalescingCache()
109112
modelsService := NewModelsService(coalescingCache, httpClient)
110113

111114
// Create proxy service
112-
proxyService := NewProxyService(cfg, httpClient, workerPool)
115+
proxyService := NewProxyService(cfg, httpClient, authService, workerPool)
113116

114117
// Create health checker
115118
healthChecker := NewHealthChecker(httpClient, "dev") // TODO: get version from build

0 commit comments

Comments
 (0)