feat: implement proto-gen platform

- Config loader with env var expansion and validation
- SQLite database with tasks and toolchain_versions tables
- Gitea REST API client for repository and branch operations
- K8s client for Job lifecycle management (with noop fallback for local dev)
- REST API: tasks CRUD, retry, plugins, version management
- Worker/scheduler with polling, concurrency control, and job tracking
- Static frontend: generate, tasks, settings pages
- Toolchain images: Go and Node.js Dockerfiles with entrypoint scripts
- Helm chart with deployment, service, configmap, ingress, PVC
- Multi-stage Dockerfile for service
- Unit tests for config, database, gitclient, k8s, api, worker
- E2E tests for idempotency, version switch, failure recovery
This commit is contained in:
2026-08-10 16:54:30 +08:00
parent cc478bdad4
commit 4e467e1136
43 changed files with 3179 additions and 1 deletions
+127
View File
@@ -0,0 +1,127 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoad_MissingFile(t *testing.T) {
_, err := Load("/nonexistent/config.yaml")
if err == nil {
t.Fatal("expected error for missing file")
}
}
func TestLoad_MissingGitToken(t *testing.T) {
cfg := `server:
addr: ":8080"
git:
type: gitea
base_url: "https://git.example.com"
cluster:
namespace: proto-gen`
path := writeTempConfig(t, cfg)
os.Setenv("GIT_TOKEN", "")
_, err := Load(path)
if err == nil {
t.Fatal("expected error for missing git.token")
}
}
func TestLoad_ValidConfig(t *testing.T) {
os.Setenv("GIT_TOKEN", "test-token")
defer os.Unsetenv("GIT_TOKEN")
cfg := `server:
addr: ":9090"
git:
type: gitea
base_url: "https://git.example.com"
token: ${GIT_TOKEN}
cluster:
namespace: proto-gen
job_timeout: 300
max_concurrent: 3
plugins:
- language: go
image: "proto-gen-go:v1.0"`
path := writeTempConfig(t, cfg)
got, err := Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Server.Addr != ":9090" {
t.Errorf("Server.Addr = %q, want %q", got.Server.Addr, ":9090")
}
if got.Git.Token != "test-token" {
t.Errorf("Git.Token = %q, want %q", got.Git.Token, "test-token")
}
if got.Cluster.JobTimeout != 300 {
t.Errorf("Cluster.JobTimeout = %d, want 300", got.Cluster.JobTimeout)
}
if len(got.Plugins) != 1 {
t.Fatalf("len(Plugins) = %d, want 1", len(got.Plugins))
}
if got.Plugins[0].Language != "go" {
t.Errorf("Plugins[0].Language = %q, want %q", got.Plugins[0].Language, "go")
}
}
func TestLoad_DefaultValues(t *testing.T) {
os.Setenv("GIT_TOKEN", "test-token")
defer os.Unsetenv("GIT_TOKEN")
cfg := `git:
type: gitea
base_url: "https://git.example.com"
token: ${GIT_TOKEN}
cluster:
namespace: proto-gen`
path := writeTempConfig(t, cfg)
got, err := Load(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Server.Addr != ":8080" {
t.Errorf("Server.Addr = %q, want %q", got.Server.Addr, ":8080")
}
if got.Cluster.JobTimeout != 600 {
t.Errorf("Cluster.JobTimeout = %d, want 600", got.Cluster.JobTimeout)
}
if got.Cluster.MaxConcurrent != 2 {
t.Errorf("Cluster.MaxConcurrent = %d, want 2", got.Cluster.MaxConcurrent)
}
if got.TargetRepoTemplate != "{proto_repo}-gen-{lang}" {
t.Errorf("TargetRepoTemplate = %q, want %q", got.TargetRepoTemplate, "{proto_repo}-gen-{lang}")
}
}
func TestGetPluginByLanguage(t *testing.T) {
cfg := &Config{
Plugins: []PluginConfig{
{Language: "go", Image: "go:v1"},
{Language: "nodejs", Image: "ts:v1"},
},
}
if p := cfg.GetPluginByLanguage("go"); p == nil {
t.Error("GetPluginByLanguage(go) = nil")
} else if p.Image != "go:v1" {
t.Errorf("Image = %q, want %q", p.Image, "go:v1")
}
if p := cfg.GetPluginByLanguage("python"); p != nil {
t.Error("GetPluginByLanguage(python) != nil")
}
}
func writeTempConfig(t *testing.T, content string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing temp config: %v", err)
}
return path
}