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) <noreply@anthropic.com>
This commit is contained in:
@@ -156,11 +156,17 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
monitoring-postgres:
|
monitoring-postgres:
|
||||||
condition: service_healthy
|
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: >
|
entrypoint: >
|
||||||
/bin/sh -c "
|
/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 -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_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"
|
restart: "no"
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,17 @@ spec:
|
|||||||
image: geo-rankly/migrate:latest
|
image: geo-rankly/migrate:latest
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
env:
|
env:
|
||||||
|
- name: CONFIG_PATH
|
||||||
|
value: /app/configs/config.yaml
|
||||||
- name: POSTGRES_PASSWORD
|
- name: POSTGRES_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: geo-rankly-app-secret
|
name: geo-rankly-app-secret
|
||||||
key: POSTGRES_PASSWORD
|
key: POSTGRES_PASSWORD
|
||||||
|
volumeMounts:
|
||||||
|
- name: app-config
|
||||||
|
mountPath: /app/configs
|
||||||
|
readOnly: true
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -ec
|
- -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 -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_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/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
|
||||||
|
|||||||
@@ -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'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 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 |
|
| 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.
|
||||||
|
|||||||
@@ -23,15 +23,23 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
|||||||
go build -trimpath -ldflags="-s -w" \
|
go build -trimpath -ldflags="-s -w" \
|
||||||
-o /bin/service ./cmd/${SERVICE}
|
-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) ─────────────
|
# ─── Stage 2: Migration image (bundles SQL files + migrate binary) ─────────────
|
||||||
FROM migrate/migrate:v4.19.1 AS migrate-tool
|
FROM migrate/migrate:v4.19.1 AS migrate-tool
|
||||||
|
|
||||||
FROM alpine:3.19 AS migrate
|
FROM alpine:3.19 AS migrate
|
||||||
RUN apk --no-cache add ca-certificates
|
RUN apk --no-cache add ca-certificates
|
||||||
COPY --from=migrate-tool /usr/local/bin/migrate /usr/local/bin/migrate
|
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/ /migrations/
|
||||||
COPY migrations_monitoring/ /migrations_monitoring/
|
COPY migrations_monitoring/ /migrations_monitoring/
|
||||||
COPY migrations_ops/ /migrations_ops/
|
COPY migrations_ops/ /migrations_ops/
|
||||||
|
COPY configs/ /app/configs/
|
||||||
ENTRYPOINT ["/usr/local/bin/migrate"]
|
ENTRYPOINT ["/usr/local/bin/migrate"]
|
||||||
|
|
||||||
# ─── Stage 3: Runtime image ────────────────────────────────────────────────────
|
# ─── Stage 3: Runtime image ────────────────────────────────────────────────────
|
||||||
|
|||||||
+4
-1
@@ -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
|
DB_URL ?= postgres://geo:geo_dev@localhost:5432/geo?sslmode=disable
|
||||||
MONITORING_DB_URL ?= postgres://geo:geo_dev@localhost:5433/geo_monitoring?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
|
seed: ## Insert development seed data
|
||||||
go run ./cmd/dev-seed
|
go run ./cmd/dev-seed
|
||||||
|
|
||||||
|
seed-platform-templates: ## Insert or update built-in platform templates
|
||||||
|
go run ./cmd/seed-platform-templates
|
||||||
|
|
||||||
lint: ## Run linter
|
lint: ## Run linter
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
func ensureTemplates(ctx context.Context, tx pgx.Tx) error {
|
||||||
for _, tpl := range prompts.PlatformTemplateSeeds() {
|
_, err := repository.EnsurePlatformTemplates(ctx, tx)
|
||||||
_, err := tx.Exec(ctx, `
|
return wrapSeedErr("ensure platform templates", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureBrand(ctx context.Context, tx pgx.Tx, tenantID int64) (int64, error) {
|
func ensureBrand(ctx context.Context, tx pgx.Tx, tenantID int64) (int64, error) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user