From 9c58fe2312622a96aec0569cbd601119cea06f10 Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 1 May 2026 16:01:55 +0800 Subject: [PATCH] feat(server): add production-safe platform-template seed job A fresh production environment only ran migrations, so the four built-in platform templates were never inserted unless someone manually ran dev-seed (which also writes broad demo data). Extract the platform template upsert into a reusable repository.EnsurePlatformTemplates and add a standalone cmd/seed-platform-templates that calls it. Bake the binary into the migrate image and chain it after migrate up in both docker-compose and the k3s migration job (mounting app config so the binary can dial the right database). Co-Authored-By: Claude Opus 4.7 (1M context) --- deploy/docker-compose.yaml | 8 ++- deploy/k3s/jobs.yaml | 23 +++++++ progress.md | 6 ++ server/Dockerfile | 8 +++ server/Makefile | 5 +- server/cmd/dev-seed/main.go | 29 +-------- server/cmd/seed-platform-templates/main.go | 60 +++++++++++++++++++ .../repository/platform_template_seed.go | 43 +++++++++++++ 8 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 server/cmd/seed-platform-templates/main.go create mode 100644 server/internal/tenant/repository/platform_template_seed.go diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml index fb8eb89..559908a 100644 --- a/deploy/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -156,11 +156,17 @@ services: condition: service_healthy monitoring-postgres: condition: service_healthy + environment: + CONFIG_PATH: /app/configs/config.yaml + volumes: + - ./config.yaml:/app/configs/config.yaml:ro + - ./prompts.yml:/app/configs/prompts.yml:ro entrypoint: > /bin/sh -c " /usr/local/bin/migrate -path /migrations -database 'postgres://geo:geo_dev@postgres:5432/geo?sslmode=disable' up && /usr/local/bin/migrate -path /migrations_monitoring -database 'postgres://geo:geo_dev@monitoring-postgres:5432/geo_monitoring?sslmode=disable' up && - /usr/local/bin/migrate -path /migrations_ops -database 'postgres://geo:geo_dev@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops' up + /usr/local/bin/migrate -path /migrations_ops -database 'postgres://geo:geo_dev@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops' up && + /usr/local/bin/seed-platform-templates " restart: "no" diff --git a/deploy/k3s/jobs.yaml b/deploy/k3s/jobs.yaml index d9cb135..b107c97 100644 --- a/deploy/k3s/jobs.yaml +++ b/deploy/k3s/jobs.yaml @@ -55,11 +55,17 @@ spec: image: geo-rankly/migrate:latest imagePullPolicy: IfNotPresent env: + - name: CONFIG_PATH + value: /app/configs/config.yaml - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: geo-rankly-app-secret key: POSTGRES_PASSWORD + volumeMounts: + - name: app-config + mountPath: /app/configs + readOnly: true command: - /bin/sh - -ec @@ -67,3 +73,20 @@ spec: /usr/local/bin/migrate -path /migrations -database "postgres://geo:${POSTGRES_PASSWORD}@postgres:5432/geo?sslmode=disable" up /usr/local/bin/migrate -path /migrations_monitoring -database "postgres://geo:${POSTGRES_PASSWORD}@monitoring-postgres:5432/geo_monitoring?sslmode=disable" up /usr/local/bin/migrate -path /migrations_ops -database "postgres://geo:${POSTGRES_PASSWORD}@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops" up + /usr/local/bin/seed-platform-templates + volumes: + - name: app-config + projected: + sources: + - configMap: + name: geo-rankly-app-config + items: + - key: config.yaml + path: config.yaml + - key: prompts.yml + path: prompts.yml + - secret: + name: geo-rankly-app-secret + items: + - key: config.local.yaml + path: config.local.yaml diff --git a/progress.md b/progress.md index a49becf..b26ba16 100644 --- a/progress.md +++ b/progress.md @@ -691,3 +691,9 @@ | What's the goal? | Resume the interrupted task by adding the missing `admin-web` frontend foundation on top of the finished backend | | What have I learned? | The tenant backend is stable enough to support a real login + dashboard frontend slice, and a real browser pass was valuable because it caught a submit wiring bug that static checks missed | | What have I done? | Added the pnpm frontend workspace, built `apps/admin-web`, wired auth and dashboard data to `tenant-api`, reduced the bundle size, and passed install/build/preview plus browser login verification | +## 2026-05-01T06:43:58Z - Platform template seed deployment + +- Investigated the "普通通用模版" cards shown in the screenshot. +- Confirmed the four platform templates are stored in `article_templates` and currently seeded by `server/cmd/dev-seed/main.go`. +- Confirmed production deployment jobs only run database migrations, so a fresh environment can miss the four template records unless dev seed is run manually. +- Decision: add a production-safe platform-template initializer and wire it into the migrate job/image instead of running the broad dev seed path. diff --git a/server/Dockerfile b/server/Dockerfile index faf855e..f44dc6e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -23,15 +23,23 @@ RUN --mount=type=cache,target=/go/pkg/mod \ go build -trimpath -ldflags="-s -w" \ -o /bin/service ./cmd/${SERVICE} +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=1 GOOS="${TARGETOS:-$(go env GOOS)}" GOARCH="${TARGETARCH:-$(go env GOARCH)}" \ + go build -trimpath -ldflags="-s -w" \ + -o /bin/seed-platform-templates ./cmd/seed-platform-templates + # ─── Stage 2: Migration image (bundles SQL files + migrate binary) ───────────── FROM migrate/migrate:v4.19.1 AS migrate-tool FROM alpine:3.19 AS migrate RUN apk --no-cache add ca-certificates COPY --from=migrate-tool /usr/local/bin/migrate /usr/local/bin/migrate +COPY --from=builder /bin/seed-platform-templates /usr/local/bin/seed-platform-templates COPY migrations/ /migrations/ COPY migrations_monitoring/ /migrations_monitoring/ COPY migrations_ops/ /migrations_ops/ +COPY configs/ /app/configs/ ENTRYPOINT ["/usr/local/bin/migrate"] # ─── Stage 3: Runtime image ──────────────────────────────────────────────────── diff --git a/server/Makefile b/server/Makefile index 503719b..b993cbc 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,4 +1,4 @@ -.PHONY: dev-init dev-api dev-worker-generate dev-scheduler dev-ops-api migrate-up migrate-down migrate-monitoring-up migrate-monitoring-down migrate-ops-up migrate-ops-down migrate-create migrate-ops-create sqlc-generate tenant-scope-guard lint test tidy seed +.PHONY: dev-init dev-api dev-worker-generate dev-scheduler dev-ops-api migrate-up migrate-down migrate-monitoring-up migrate-monitoring-down migrate-ops-up migrate-ops-down migrate-create migrate-ops-create sqlc-generate tenant-scope-guard lint test tidy seed seed-platform-templates DB_URL ?= postgres://geo:geo_dev@localhost:5432/geo?sslmode=disable MONITORING_DB_URL ?= postgres://geo:geo_dev@localhost:5433/geo_monitoring?sslmode=disable @@ -61,6 +61,9 @@ tenant-scope-guard: ## Ensure tenant-scoped SQL includes tenant_id filters seed: ## Insert development seed data go run ./cmd/dev-seed +seed-platform-templates: ## Insert or update built-in platform templates + go run ./cmd/seed-platform-templates + lint: ## Run linter golangci-lint run ./... diff --git a/server/cmd/dev-seed/main.go b/server/cmd/dev-seed/main.go index e1a8ae5..6507080 100644 --- a/server/cmd/dev-seed/main.go +++ b/server/cmd/dev-seed/main.go @@ -485,33 +485,8 @@ func ensureQuotaLedger(ctx context.Context, tx pgx.Tx, tenantID int64, total int } func ensureTemplates(ctx context.Context, tx pgx.Tx) error { - for _, tpl := range prompts.PlatformTemplateSeeds() { - _, err := tx.Exec(ctx, ` - INSERT INTO article_templates (scope, origin_type, template_key, template_name, prompt_template, prompt_visibility, card_config_json, status) - VALUES ('platform', 'platform', $1, $2, $3, 'visible', $4::jsonb, 'active') - ON CONFLICT DO NOTHING - `, tpl.Key, tpl.Name, tpl.PromptTemplate, tpl.CardConfigJSON) - if err != nil { - return wrapSeedErr("insert template", err) - } - - _, err = tx.Exec(ctx, ` - UPDATE article_templates - SET template_name = $2, - prompt_template = $3, - card_config_json = $4::jsonb, - prompt_visibility = 'visible', - status = 'active', - updated_at = NOW() - WHERE scope = 'platform' - AND template_key = $1 - AND deleted_at IS NULL - `, tpl.Key, tpl.Name, tpl.PromptTemplate, tpl.CardConfigJSON) - if err != nil { - return wrapSeedErr("update template", err) - } - } - return nil + _, err := repository.EnsurePlatformTemplates(ctx, tx) + return wrapSeedErr("ensure platform templates", err) } func ensureBrand(ctx context.Context, tx pgx.Tx, tenantID int64) (int64, error) { diff --git a/server/cmd/seed-platform-templates/main.go b/server/cmd/seed-platform-templates/main.go new file mode 100644 index 0000000..3b006c0 --- /dev/null +++ b/server/cmd/seed-platform-templates/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/geo-platform/tenant-api/internal/shared/config" + "github.com/geo-platform/tenant-api/internal/tenant/prompts" + "github.com/geo-platform/tenant-api/internal/tenant/repository" +) + +func main() { + configPath := "configs/config.yaml" + if p := strings.TrimSpace(os.Getenv("CONFIG_PATH")); p != "" { + configPath = p + } + + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("load config: %v", err) + } + if password := strings.TrimSpace(os.Getenv("POSTGRES_PASSWORD")); password != "" { + cfg.Database.Password = password + } + prompts.SetConfigFile(filepath.Join(filepath.Dir(configPath), "prompts.yml")) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + pool, err := pgxpool.New(ctx, cfg.Database.DSN()) + if err != nil { + log.Fatalf("connect database: %v", err) + } + defer pool.Close() + + tx, err := pool.Begin(ctx) + if err != nil { + log.Fatalf("begin transaction: %v", err) + } + defer func() { + _ = tx.Rollback(ctx) + }() + + count, err := repository.EnsurePlatformTemplates(ctx, tx) + if err != nil { + log.Fatalf("ensure platform templates: %v", err) + } + if err := tx.Commit(ctx); err != nil { + log.Fatalf("commit platform templates: %v", err) + } + + fmt.Printf("Platform templates ensured: %d\n", count) +} diff --git a/server/internal/tenant/repository/platform_template_seed.go b/server/internal/tenant/repository/platform_template_seed.go new file mode 100644 index 0000000..a3b3151 --- /dev/null +++ b/server/internal/tenant/repository/platform_template_seed.go @@ -0,0 +1,43 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/geo-platform/tenant-api/internal/tenant/prompts" + "github.com/geo-platform/tenant-api/internal/tenant/repository/generated" +) + +func EnsurePlatformTemplates(ctx context.Context, db generated.DBTX) (int, error) { + seeds := prompts.PlatformTemplateSeeds() + for _, tpl := range seeds { + _, err := db.Exec(ctx, ` + INSERT INTO article_templates ( + scope, + tenant_id, + origin_type, + template_key, + template_name, + prompt_template, + prompt_visibility, + card_config_json, + status, + version_no + ) + VALUES ('platform', NULL, 'platform', $1, $2, $3, 'visible', $4::jsonb, 'active', 1) + ON CONFLICT (scope, template_key, version_no) + WHERE deleted_at IS NULL AND tenant_id IS NULL + DO UPDATE SET + template_name = EXCLUDED.template_name, + prompt_template = EXCLUDED.prompt_template, + card_config_json = EXCLUDED.card_config_json, + prompt_visibility = 'visible', + status = 'active', + updated_at = NOW() + `, tpl.Key, tpl.Name, tpl.PromptTemplate, tpl.CardConfigJSON) + if err != nil { + return 0, fmt.Errorf("upsert platform template %q: %w", tpl.Key, err) + } + } + return len(seeds), nil +}