From 967c3e42097546d179c411a2861ac6e8cc362176 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 3 Sep 2026 10:08:11 +0800 Subject: [PATCH] feat(contracts): add TableName and gorm serializer tags to DTOs --- .dockerignore | 1 + .github/workflows/build-image.yml | 2 +- Makefile | 2 +- README.md | 2 +- README_zh.md | 2 +- backend/core/contracts/config.go | 19 ++-- backend/core/contracts/task.go | 5 ++ backend/core/contracts/upload.go | 23 +++-- .../plugins/domain/admin/service/config.go | 9 ++ backend/plugins/domain/upload/service.go | 18 ++-- .../driver_inproc_worker/task_service.go | 19 ++-- backend/plugins/infra/config/source.go | 35 +++++--- backend/plugins/infra/config/source_test.go | 29 +++++++ docker-compose.yml | 2 +- docker/Dockerfile | 77 ---------------- .../deploy/kustomize/base/deployment.yaml | 15 ++-- manifest/deploy/kustomize/base/service.yaml | 4 +- .../kustomize/overlays/develop/configmap.yaml | 19 ++-- .../overlays/develop/deployment.yaml | 6 +- .../overlays/develop/kustomization.yaml | 4 +- manifest/docker/Dockerfile | 87 ++++++++++++++++--- .../docker}/Dockerfile.backend | 0 {docker => manifest/docker}/Dockerfile.cross | 12 ++- .../docker}/Dockerfile.frontend | 0 manifest/i18n/.gitkeep | 0 manifest/protobuf/.keep-if-necessary | 0 26 files changed, 221 insertions(+), 171 deletions(-) delete mode 100644 docker/Dockerfile rename {docker => manifest/docker}/Dockerfile.backend (100%) rename {docker => manifest/docker}/Dockerfile.cross (92%) rename {docker => manifest/docker}/Dockerfile.frontend (100%) delete mode 100755 manifest/i18n/.gitkeep delete mode 100755 manifest/protobuf/.keep-if-necessary diff --git a/.dockerignore b/.dockerignore index 65420584..6a369bb3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ Thumbs.db config.yaml +manifest/config/config.yaml .env .env.* diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 16adf69d..4dcda192 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -24,7 +24,7 @@ permissions: env: IMAGE_NAME: wavelet - DOCKERFILE: docker/Dockerfile + DOCKERFILE: manifest/docker/Dockerfile jobs: # Resolve version / registries once. No checkout: triggers alone determine the tag. diff --git a/Makefile b/Makefile index a6c323d7..94711953 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,7 @@ cross-build: (version=$(or $(VERSION),dev))..." @mkdir -p bin docker build \ - --file docker/Dockerfile.cross \ + --file manifest/docker/Dockerfile.cross \ --target export \ --build-arg VERSION=$(or $(VERSION),dev) \ --build-arg BUILD_DATE="$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" \ diff --git a/README.md b/README.md index 26c5bdbf..cd24b851 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ wavelet/ ├── main.go # Entry point (delegates to internal/cmd) ├── config.example.yaml # Configuration template ├── Makefile # Common commands (swagger, tidy, license, cross-build) -├── docker/ # Docker image build files (integrated/frontend/backend) +├── manifest/ # Manifests: docker (compose/Dockerfiles), deploy (k8s), config ├── docs/ # Swagger auto-generated docs ├── frontend/ # Next.js frontend application │ ├── app/ # App Router pages diff --git a/README_zh.md b/README_zh.md index 686af6b6..a5cdb71c 100644 --- a/README_zh.md +++ b/README_zh.md @@ -213,7 +213,7 @@ wavelet/ ├── main.go # 程序入口(委托给 internal/cmd) ├── config.example.yaml # 配置模板 ├── Makefile # 常用命令(swagger、tidy、license、cross-build) -├── docker/ # Docker 镜像构建文件(集成/前端/后端) +├── manifest/ # 项目清单与编排:docker 镜像构建/compose、deploy (k8s)、config 配置 ├── docs/ # Swagger 自动生成文档 ├── frontend/ # Next.js 前端应用 │ ├── app/ # App Router 页面 diff --git a/backend/core/contracts/config.go b/backend/core/contracts/config.go index 0a17928c..2ffdef2e 100644 --- a/backend/core/contracts/config.go +++ b/backend/core/contracts/config.go @@ -10,13 +10,18 @@ import ( // SystemConfigDTO represents a system configuration key-value entry. type SystemConfigDTO struct { - Key string `json:"key"` - Value string `json:"value"` - Type string `json:"type"` - Visibility int `json:"visibility"` - Description string `json:"description"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + Key string `json:"key" gorm:"primaryKey;column:key;size:255"` + Value string `json:"value" gorm:"column:value;type:text"` + Type string `json:"type" gorm:"column:type;size:50"` + Visibility int `json:"visibility" gorm:"column:visibility"` + Description string `json:"description" gorm:"column:description;size:500"` + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` +} + +// TableName returns the default table name for SystemConfigDTO. +func (SystemConfigDTO) TableName() string { + return "w_system_configs" } // SystemConfigService defines the unified contract for querying and mutating system configurations. diff --git a/backend/core/contracts/task.go b/backend/core/contracts/task.go index efd05de6..cf9bac13 100644 --- a/backend/core/contracts/task.go +++ b/backend/core/contracts/task.go @@ -71,6 +71,11 @@ type TaskExecutionDTO struct { UpdatedAt time.Time `json:"updated_at"` } +// TableName returns the default table name for TaskExecutionDTO. +func (TaskExecutionDTO) TableName() string { + return "w_task_executions" +} + // Canonical triggered_by values persisted on task executions and shown in admin UI. const ( TaskTriggerSystem = "system" diff --git a/backend/core/contracts/upload.go b/backend/core/contracts/upload.go index 0bb5b4d6..3798edcd 100644 --- a/backend/core/contracts/upload.go +++ b/backend/core/contracts/upload.go @@ -23,20 +23,25 @@ type UploadMetadataDTO struct { // UploadDTO represents an uploaded file record. type UploadDTO struct { - ID uint64 `json:"id"` - UserID uint64 `json:"user_id"` - FileName string `json:"file_name"` - FilePath string `json:"file_path"` - MimeType string `json:"mime_type"` + ID uint64 `json:"id" gorm:"primaryKey"` + UserID uint64 `json:"user_id" gorm:"index"` + FileName string `json:"file_name" gorm:"size:255"` + FilePath string `json:"file_path" gorm:"size:500"` + MimeType string `json:"mime_type" gorm:"size:100"` Size int64 `json:"size"` - Hash string `json:"hash"` - Status string `json:"status"` - Type string `json:"type"` - Metadata UploadMetadataDTO `json:"metadata"` + Hash string `json:"hash" gorm:"size:64"` + Status string `json:"status" gorm:"type:varchar(20)"` + Type string `json:"type" gorm:"size:50;index"` + Metadata UploadMetadataDTO `json:"metadata" gorm:"serializer:json;type:jsonb"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } +// TableName returns the default table name for UploadDTO. +func (UploadDTO) TableName() string { + return "w_uploads" +} + // OpenedUploadDTO encapsulates the retrieved object stream and its metadata. type OpenedUploadDTO struct { Upload UploadDTO diff --git a/backend/plugins/domain/admin/service/config.go b/backend/plugins/domain/admin/service/config.go index b069b511..6590ae1d 100644 --- a/backend/plugins/domain/admin/service/config.go +++ b/backend/plugins/domain/admin/service/config.go @@ -29,6 +29,7 @@ func (PublicConfigAdapter) PublicConfig(ctx context.Context) (map[string]string, // SystemConfigServiceImpl implements contracts.SystemConfigService. type SystemConfigServiceImpl struct{} +// GetByKey retrieves a system config by its unique key. func (SystemConfigServiceImpl) GetByKey(ctx context.Context, key string) (contracts.SystemConfigDTO, error) { cfg, err := repository.GetSystemConfigByKey(ctx, key) if err != nil { @@ -37,6 +38,7 @@ func (SystemConfigServiceImpl) GetByKey(ctx context.Context, key string) (contra return toSystemConfigDTO(cfg), nil } +// ListByKeys retrieves multiple system configs by their keys. func (SystemConfigServiceImpl) ListByKeys(ctx context.Context, keys []string) (map[string]contracts.SystemConfigDTO, error) { cfgs, err := repository.ListSystemConfigsByKeys(ctx, keys) if err != nil { @@ -49,6 +51,7 @@ func (SystemConfigServiceImpl) ListByKeys(ctx context.Context, keys []string) (m return res, nil } +// ListVisible returns all user-visible system configs. func (SystemConfigServiceImpl) ListVisible(ctx context.Context) ([]contracts.SystemConfigDTO, error) { cfgs, err := repository.ListVisibleSystemConfigs(ctx) if err != nil { @@ -61,6 +64,7 @@ func (SystemConfigServiceImpl) ListVisible(ctx context.Context) ([]contracts.Sys return res, nil } +// ListByType returns all system configs belonging to a specific configuration type. func (SystemConfigServiceImpl) ListByType(ctx context.Context, configType string) ([]contracts.SystemConfigDTO, error) { cfgs, err := repository.ListAdminSystemConfigs(ctx, configType) if err != nil { @@ -73,22 +77,27 @@ func (SystemConfigServiceImpl) ListByType(ctx context.Context, configType string return res, nil } +// GetIntByKey retrieves an integer system config value. func (SystemConfigServiceImpl) GetIntByKey(ctx context.Context, key string) (int, error) { return repository.GetIntByKey(ctx, key) } +// GetBoolByKey retrieves a boolean system config value. func (SystemConfigServiceImpl) GetBoolByKey(ctx context.Context, key string) (bool, error) { return repository.GetBoolByKey(ctx, key) } +// SaveOrUpdate persists or updates a system config key-value pair. func (SystemConfigServiceImpl) SaveOrUpdate(ctx context.Context, key, value string) error { return repository.SaveOrUpdateSystemConfig(ctx, key, value) } +// InvalidateCache evicts the cache entry for the specified key. func (SystemConfigServiceImpl) InvalidateCache(ctx context.Context, key string) error { return repository.InvalidateSystemConfigCache(ctx, key) } +// InvalidateAllCaches purges all system configuration cache entries. func (SystemConfigServiceImpl) InvalidateAllCaches(ctx context.Context) error { return repository.InvalidateAllSystemConfigCaches(ctx) } diff --git a/backend/plugins/domain/upload/service.go b/backend/plugins/domain/upload/service.go index 7abdf002..9374eb00 100644 --- a/backend/plugins/domain/upload/service.go +++ b/backend/plugins/domain/upload/service.go @@ -69,15 +69,15 @@ func (s *uploadServiceImpl) RebuildStats(ctx context.Context) error { func toUploadDTO(u *models.Upload) contracts.UploadDTO { return contracts.UploadDTO{ - ID: u.ID, - UserID: u.UserID, - FileName: u.FileName, - FilePath: u.FilePath, - MimeType: u.MimeType, - Size: u.FileSize, - Hash: u.Hash, - Status: string(u.Status), - Type: u.Type, + ID: u.ID, + UserID: u.UserID, + FileName: u.FileName, + FilePath: u.FilePath, + MimeType: u.MimeType, + Size: u.FileSize, + Hash: u.Hash, + Status: string(u.Status), + Type: u.Type, Metadata: contracts.UploadMetadataDTO{ Width: u.Metadata.Width, Height: u.Metadata.Height, diff --git a/backend/plugins/drivers/driver_inproc_worker/task_service.go b/backend/plugins/drivers/driver_inproc_worker/task_service.go index a5eadc50..41545179 100644 --- a/backend/plugins/drivers/driver_inproc_worker/task_service.go +++ b/backend/plugins/drivers/driver_inproc_worker/task_service.go @@ -112,30 +112,25 @@ func (s *inprocTaskService) ListExecutions(ctx context.Context, taskType, status return res, total, nil } -func (s *inprocTaskService) GetExecution(ctx context.Context, id uint64) (*contracts.TaskExecutionDTO, error) { +func findExecution(ctx context.Context, query any, args ...any) (*contracts.TaskExecutionDTO, error) { db := getDB(ctx) if db == nil { return nil, errors.New("driver_inproc_worker: db not initialized") } var exec taskExecution - if err := db.Where("id = ?", id).First(&exec).Error; err != nil { + if err := db.Where(query, args...).First(&exec).Error; err != nil { return nil, err } dto := toExecutionDTO(&exec) return &dto, nil } +func (s *inprocTaskService) GetExecution(ctx context.Context, id uint64) (*contracts.TaskExecutionDTO, error) { + return findExecution(ctx, "id = ?", id) +} + func (s *inprocTaskService) GetExecutionByTaskID(ctx context.Context, taskID string) (*contracts.TaskExecutionDTO, error) { - db := getDB(ctx) - if db == nil { - return nil, errors.New("driver_inproc_worker: db not initialized") - } - var exec taskExecution - if err := db.Where("task_id = ?", taskID).First(&exec).Error; err != nil { - return nil, err - } - dto := toExecutionDTO(&exec) - return &dto, nil + return findExecution(ctx, "task_id = ?", taskID) } func toExecutionDTO(exec *taskExecution) contracts.TaskExecutionDTO { diff --git a/backend/plugins/infra/config/source.go b/backend/plugins/infra/config/source.go index fd6c711f..1b3525fd 100644 --- a/backend/plugins/infra/config/source.go +++ b/backend/plugins/infra/config/source.go @@ -19,6 +19,13 @@ import ( // DefaultFileName is the configuration file looked up when CONFIG_PATH is unset. const DefaultFileName = "config.yaml" +// DefaultCandidates defines the prioritized list of configuration file paths looked up +// when CONFIG_PATH is unset. +var DefaultCandidates = []string{ + "manifest/config/config.yaml", + DefaultFileName, +} + // EnvOnlyOrigin is reported by Describe when no configuration file was loaded. const EnvOnlyOrigin = "" @@ -56,7 +63,7 @@ func NewSource(opts ...Option) (*Source, error) { s.path = os.Getenv("CONFIG_PATH") } if s.path == "" { - s.path = findConfigPath(DefaultFileName) + s.path = findConfigPath(DefaultCandidates...) } v := viper.New() @@ -107,20 +114,28 @@ func (s *Source) Describe() string { return s.path } -// findConfigPath searches upward from the working directory so tests and binaries run -// from backend/ still find the repository-root configuration file. -func findConfigPath(configPath string) string { - if _, err := os.Stat(configPath); err == nil { - return configPath +// findConfigPath searches upward from the working directory through candidate paths so +// tests and binaries run from subdirectories still find configuration files in manifest/config/ +// or the repository root. +func findConfigPath(candidates ...string) string { + if len(candidates) == 0 { + return DefaultFileName + } + for _, candidate := range candidates { + if _, err := os.Stat(candidate); err == nil { + return candidate + } } dir := "." for range maxSearchDepth { dir += "/.." - path := dir + "/" + configPath - if _, err := os.Stat(path); err == nil { - return path + for _, candidate := range candidates { + path := dir + "/" + candidate + if _, err := os.Stat(path); err == nil { + return path + } } } - return configPath + return candidates[0] } diff --git a/backend/plugins/infra/config/source_test.go b/backend/plugins/infra/config/source_test.go index 8372e156..f8fff81a 100644 --- a/backend/plugins/infra/config/source_test.go +++ b/backend/plugins/infra/config/source_test.go @@ -91,3 +91,32 @@ func TestSourcePrefersConfigPathEnvironmentVariable(t *testing.T) { require.True(t, ok) assert.Equal(t, ":9100", value) } + +func TestSourceFindsManifestConfigInParentDirectory(t *testing.T) { + tempRoot := t.TempDir() + manifestConfigDir := filepath.Join(tempRoot, "manifest", "config") + require.NoError(t, os.MkdirAll(manifestConfigDir, 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(manifestConfigDir, "config.yaml"), + []byte("app:\n addr: \":9200\"\n"), + 0o600, + )) + + subDir := filepath.Join(tempRoot, "backend", "cmd") + require.NoError(t, os.MkdirAll(subDir, 0o755)) + + origWd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(subDir)) + t.Cleanup(func() { + _ = os.Chdir(origWd) + }) + + t.Setenv("CONFIG_PATH", "") + + src, err := config.NewSource() + require.NoError(t, err) + value, ok := src.Lookup("app.addr") + require.True(t, ok) + assert.Equal(t, ":9200", value) +} diff --git a/docker-compose.yml b/docker-compose.yml index fae282c5..64b2a496 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ services: wavelet: build: context: . - dockerfile: docker/Dockerfile + dockerfile: manifest/docker/Dockerfile args: VERSION: canary image: ghcr.io/rain-kl/wavelet:canary diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index f6148c66..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,77 +0,0 @@ -# syntax=docker/dockerfile:1.7 - -ARG GO_VERSION=1.25 -ARG NODE_VERSION=22 -ARG ALPINE_VERSION=3.23 - -FROM node:${NODE_VERSION}-alpine AS frontend-builder - -ARG VERSION=dev -ARG BUILD_DATE="" - -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -ENV NODE_ENV=production -ENV NEXT_STANDALONE_EXPORT=true -ENV NEXT_PUBLIC_APP_VERSION=${VERSION} -ENV NEXT_PUBLIC_APP_BUILD_DATE=${BUILD_DATE} - -RUN corepack enable && corepack prepare pnpm@10.10.0 --activate - -WORKDIR /workspace/frontend - -COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./ -RUN pnpm install --frozen-lockfile - -COPY frontend/ ./ - -# Assert index.html so a silent export failure cannot ship an API-only binary. -RUN pnpm build:embed && test -f out/index.html - -FROM golang:${GO_VERSION}-alpine AS backend-builder - -ARG VERSION=dev -ARG BUILD_DATE="" - -RUN apk add --no-cache ca-certificates git - -WORKDIR /workspace - -# Cache dependency layer -COPY backend/go.mod backend/go.sum ./backend/ -RUN cd backend && go mod download - -COPY . . - -# Drop any stale bundle carried in the context, then overlay the fresh export so -# `//go:embed all:dist` in driver_http picks up exactly this frontend build. -RUN rm -rf backend/plugins/drivers/driver_http/dist -COPY --from=frontend-builder /workspace/frontend/out backend/plugins/drivers/driver_http/dist - -WORKDIR /workspace/backend - -RUN test -f plugins/drivers/driver_http/dist/index.html && \ - CGO_ENABLED=0 GOOS=linux go build \ - -tags embed_frontend \ - -trimpath \ - -ldflags="-s -w -X Wavelet/pkg/buildinfo.Version=${VERSION} -X Wavelet/pkg/buildinfo.BuildTime=${BUILD_DATE}" \ - -o /out/wavelet \ - ./main.go - -FROM alpine:${ALPINE_VERSION} - -ARG TZ=Asia/Shanghai - -RUN apk add --no-cache ca-certificates tzdata postgresql-client && \ - cp /usr/share/zoneinfo/${TZ} /etc/localtime && \ - echo "${TZ}" > /etc/timezone - -WORKDIR /app - -COPY --from=backend-builder /out/wavelet ./wavelet -COPY docs ./docs - -EXPOSE 8000 - -ENTRYPOINT ["./wavelet"] -CMD ["all"] diff --git a/manifest/deploy/kustomize/base/deployment.yaml b/manifest/deploy/kustomize/base/deployment.yaml index 28f1d694..a159fe9f 100755 --- a/manifest/deploy/kustomize/base/deployment.yaml +++ b/manifest/deploy/kustomize/base/deployment.yaml @@ -1,21 +1,24 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: template-single + name: wavelet labels: - app: template-single + app: wavelet spec: replicas: 1 selector: matchLabels: - app: template-single + app: wavelet template: metadata: labels: - app: template-single + app: wavelet spec: containers: - - name : main - image: template-single + - name: wavelet + image: wavelet imagePullPolicy: Always + ports: + - containerPort: 8000 + name: http diff --git a/manifest/deploy/kustomize/base/service.yaml b/manifest/deploy/kustomize/base/service.yaml index 608771cc..9631b8f5 100755 --- a/manifest/deploy/kustomize/base/service.yaml +++ b/manifest/deploy/kustomize/base/service.yaml @@ -1,12 +1,12 @@ apiVersion: v1 kind: Service metadata: - name: template-single + name: wavelet spec: ports: - port: 80 protocol: TCP targetPort: 8000 selector: - app: template-single + app: wavelet diff --git a/manifest/deploy/kustomize/overlays/develop/configmap.yaml b/manifest/deploy/kustomize/overlays/develop/configmap.yaml index 3b1d0afe..17d49782 100755 --- a/manifest/deploy/kustomize/overlays/develop/configmap.yaml +++ b/manifest/deploy/kustomize/overlays/develop/configmap.yaml @@ -1,14 +1,15 @@ apiVersion: v1 kind: ConfigMap metadata: - name: template-single-configmap + name: wavelet-configmap data: config.yaml: | - server: - address: ":8000" - openapiPath: "/api.json" - swaggerPath: "/swagger" - - logger: - level : "all" - stdout: true + app: + env: "development" + addr: ":8000" + node_id: 1 + database: + enabled: false + sqlite_path: "data/wavelet.db" + redis: + enabled: false diff --git a/manifest/deploy/kustomize/overlays/develop/deployment.yaml b/manifest/deploy/kustomize/overlays/develop/deployment.yaml index 04e48519..66d83161 100755 --- a/manifest/deploy/kustomize/overlays/develop/deployment.yaml +++ b/manifest/deploy/kustomize/overlays/develop/deployment.yaml @@ -1,10 +1,10 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: template-single + name: wavelet spec: template: spec: containers: - - name : main - image: template-single:develop \ No newline at end of file + - name: wavelet + image: ghcr.io/rain-kl/wavelet:canary \ No newline at end of file diff --git a/manifest/deploy/kustomize/overlays/develop/kustomization.yaml b/manifest/deploy/kustomize/overlays/develop/kustomization.yaml index 4731c47f..b598094b 100755 --- a/manifest/deploy/kustomize/overlays/develop/kustomization.yaml +++ b/manifest/deploy/kustomize/overlays/develop/kustomization.yaml @@ -5,8 +5,8 @@ resources: - ../../base - configmap.yaml -patchesStrategicMerge: -- deployment.yaml +patches: +- path: deployment.yaml namespace: default diff --git a/manifest/docker/Dockerfile b/manifest/docker/Dockerfile index d3abe8f5..f6148c66 100755 --- a/manifest/docker/Dockerfile +++ b/manifest/docker/Dockerfile @@ -1,16 +1,77 @@ -FROM loads/alpine:3.8 +# syntax=docker/dockerfile:1.7 -############################################################################### -# INSTALLATION -############################################################################### +ARG GO_VERSION=1.25 +ARG NODE_VERSION=22 +ARG ALPINE_VERSION=3.23 -ENV WORKDIR /app -ADD resource $WORKDIR/ -ADD ./temp/linux_amd64/main $WORKDIR/main -RUN chmod +x $WORKDIR/main +FROM node:${NODE_VERSION}-alpine AS frontend-builder -############################################################################### -# START -############################################################################### -WORKDIR $WORKDIR -CMD ./main +ARG VERSION=dev +ARG BUILD_DATE="" + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +ENV NODE_ENV=production +ENV NEXT_STANDALONE_EXPORT=true +ENV NEXT_PUBLIC_APP_VERSION=${VERSION} +ENV NEXT_PUBLIC_APP_BUILD_DATE=${BUILD_DATE} + +RUN corepack enable && corepack prepare pnpm@10.10.0 --activate + +WORKDIR /workspace/frontend + +COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY frontend/ ./ + +# Assert index.html so a silent export failure cannot ship an API-only binary. +RUN pnpm build:embed && test -f out/index.html + +FROM golang:${GO_VERSION}-alpine AS backend-builder + +ARG VERSION=dev +ARG BUILD_DATE="" + +RUN apk add --no-cache ca-certificates git + +WORKDIR /workspace + +# Cache dependency layer +COPY backend/go.mod backend/go.sum ./backend/ +RUN cd backend && go mod download + +COPY . . + +# Drop any stale bundle carried in the context, then overlay the fresh export so +# `//go:embed all:dist` in driver_http picks up exactly this frontend build. +RUN rm -rf backend/plugins/drivers/driver_http/dist +COPY --from=frontend-builder /workspace/frontend/out backend/plugins/drivers/driver_http/dist + +WORKDIR /workspace/backend + +RUN test -f plugins/drivers/driver_http/dist/index.html && \ + CGO_ENABLED=0 GOOS=linux go build \ + -tags embed_frontend \ + -trimpath \ + -ldflags="-s -w -X Wavelet/pkg/buildinfo.Version=${VERSION} -X Wavelet/pkg/buildinfo.BuildTime=${BUILD_DATE}" \ + -o /out/wavelet \ + ./main.go + +FROM alpine:${ALPINE_VERSION} + +ARG TZ=Asia/Shanghai + +RUN apk add --no-cache ca-certificates tzdata postgresql-client && \ + cp /usr/share/zoneinfo/${TZ} /etc/localtime && \ + echo "${TZ}" > /etc/timezone + +WORKDIR /app + +COPY --from=backend-builder /out/wavelet ./wavelet +COPY docs ./docs + +EXPOSE 8000 + +ENTRYPOINT ["./wavelet"] +CMD ["all"] diff --git a/docker/Dockerfile.backend b/manifest/docker/Dockerfile.backend similarity index 100% rename from docker/Dockerfile.backend rename to manifest/docker/Dockerfile.backend diff --git a/docker/Dockerfile.cross b/manifest/docker/Dockerfile.cross similarity index 92% rename from docker/Dockerfile.cross rename to manifest/docker/Dockerfile.cross index 4b411d52..b7953279 100644 --- a/docker/Dockerfile.cross +++ b/manifest/docker/Dockerfile.cross @@ -8,18 +8,16 @@ # TARGET_ARCH — restrict to one arch: amd64 | arm64 (default: all) # # Usage: -# docker build -f docker/Dockerfile.cross --output ./bin . -# docker build -f docker/Dockerfile.cross \ -# --build-arg VERSION=v1.2.3 --output ./bin . -# docker build -f docker/Dockerfile.cross \ -# --build-arg TARGET_OS=linux --build-arg TARGET_ARCH=arm64 \ -# --output ./bin . +# docker build -f manifest/docker/Dockerfile.cross --output ./bin . +# docker build -f manifest/docker/Dockerfile.cross \ +# --build-arg TARGET_OS=linux --build-arg TARGET_ARCH=amd64 --output ./bin . +# docker build -f manifest/docker/Dockerfile.cross \ +# --build-arg TARGET_OS=windows --build-arg TARGET_ARCH=amd64 --output ./bin . # # Or via Makefile: # make cross-build # make cross-build VERSION=v1.2.3 # make cross-build GOOS=linux GOARCH=arm64 -# make cross-build GOOS=darwin ARG GO_VERSION=1.25 ARG NODE_VERSION=22 diff --git a/docker/Dockerfile.frontend b/manifest/docker/Dockerfile.frontend similarity index 100% rename from docker/Dockerfile.frontend rename to manifest/docker/Dockerfile.frontend diff --git a/manifest/i18n/.gitkeep b/manifest/i18n/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/manifest/protobuf/.keep-if-necessary b/manifest/protobuf/.keep-if-necessary deleted file mode 100755 index e69de29b..00000000