From b94e46d96621fb494fc9a49bb378199b13fdf19d Mon Sep 17 00:00:00 2001 From: longpeng Date: Sun, 30 Aug 2026 09:56:40 +0800 Subject: [PATCH] feat: add release engineering pipeline --- .github/workflows/ci.yml | 33 +++++- .github/workflows/release.yml | 178 ++++++++++++++++++++++++++++ .golangci.yml | 18 +++ .yamllint.yml | 8 ++ CHANGELOG.md | 16 +++ Dockerfile | 16 ++- Makefile | 50 +++++++- README.md | 22 +++- REMAINING_TASKS.md | 11 +- cmd/server/main.go | 16 ++- docs/release-policy.md | 49 ++++++++ go.mod | 22 ++-- go.sum | 76 ++++++------ internal/api/httpapi/server_test.go | 14 +-- internal/auth/auth_test.go | 4 +- internal/metrics/metrics_test.go | 3 +- pkg/sdk/go/client.go | 2 +- scripts/check-migrations.sh | 42 +++++++ scripts/check-release.sh | 17 +++ web/Dockerfile | 10 +- 20 files changed, 526 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .golangci.yml create mode 100644 .yamllint.yml create mode 100644 CHANGELOG.md create mode 100644 docs/release-policy.md create mode 100644 scripts/check-migrations.sh create mode 100644 scripts/check-release.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a83f52d..5ddfb7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,21 +28,23 @@ jobs: TEST_DATABASE_URL: postgres://configcenter:configcenter@127.0.0.1:5432/configcenter_test?sslmode=disable steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "24" cache: npm @@ -57,6 +59,14 @@ jobs: - name: Verify generated protobuf code run: make proto-check + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.12.2 + + - name: Go vulnerability check + run: go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./... + - name: Go tests run: GOCACHE=/tmp/configcenter-go-cache go test ./... @@ -68,3 +78,18 @@ jobs: - name: Web build run: npm --prefix web ci && npm --prefix web run build + + - name: Validate YAML, Compose and migrations + env: + MIGRATION_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }} + run: make validate-manifests + + - name: Dependency vulnerability scan + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln + severity: HIGH,CRITICAL + ignore-unfixed: false + exit-code: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..55dd876 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,178 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: configcenter_test + POSTGRES_USER: configcenter + POSTGRES_PASSWORD: configcenter + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U configcenter -d configcenter_test" + --health-interval 5s + --health-timeout 3s + --health-retries 20 + env: + TEST_DATABASE_URL: postgres://configcenter:configcenter@127.0.0.1:5432/configcenter_test?sslmode=disable + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Set up protoc + uses: arduino/setup-protoc@v3 + with: + version: "25.1" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify release tag and changelog + run: sh scripts/check-release.sh "${GITHUB_REF_NAME}" + + - name: Verify repository + run: make verify + + images: + needs: verify + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + actions: read + strategy: + fail-fast: false + matrix: + include: + - component: server + dockerfile: Dockerfile + - component: web + dockerfile: web/Dockerfile + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Prepare image name + id: image + shell: bash + run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}-${{ matrix.component }}" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Capture build timestamp + id: buildmeta + shell: bash + run: echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ steps.image.outputs.name }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + + - name: Build and push multi-architecture image + id: build + uses: docker/build-push-action@v7 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + provenance: mode=max + sbom: true + build-args: | + VERSION=${{ github.ref_name }} + VCS_REF=${{ github.sha }} + BUILD_DATE=${{ steps.buildmeta.outputs.date }} + cache-from: type=gha,scope=${{ matrix.component }} + cache-to: type=gha,mode=max,scope=${{ matrix.component }} + + - name: Scan image for HIGH/CRITICAL vulnerabilities + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: image + image-ref: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} + scanners: vuln + severity: HIGH,CRITICAL + ignore-unfixed: false + exit-code: 1 + + - name: Generate SPDX SBOM + uses: anchore/sbom-action@v0 + with: + image: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} + format: spdx-json + artifact-name: configcenter-${{ matrix.component }}-${{ github.ref_name }}.spdx.json + output-file: configcenter-${{ matrix.component }}-${{ github.ref_name }}.spdx.json + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + upload-release-assets: false + + - name: Install Cosign + uses: sigstore/cosign-installer@v4.1.0 + + - name: Sign image digest with GitHub OIDC + env: + IMAGE: ${{ steps.image.outputs.name }} + DIGEST: ${{ steps.build.outputs.digest }} + run: cosign sign --yes "${IMAGE}@${DIGEST}" + + - name: Verify keyless signature + env: + IMAGE: ${{ steps.image.outputs.name }} + DIGEST: ${{ steps.build.outputs.digest }} + REPOSITORY: ${{ github.repository }} + run: >- + cosign verify + --certificate-identity-regexp="https://github.com/${REPOSITORY}/.github/workflows/release.yml@refs/tags/v.*" + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" + "${IMAGE}@${DIGEST}" diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..0fc8a28 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,18 @@ +version: "2" + +run: + timeout: 5m + +linters: + default: standard + enable: + - bodyclose + - errorlint + - gosec + - misspell + - nilerr + - noctx + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..221a96f --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,8 @@ +extends: default + +rules: + braces: disable + document-start: disable + line-length: disable + truthy: + allowed-values: ["true", "false", "on"] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3524826 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to Config Center are recorded in this file. Releases follow Semantic Versioning. + +## [Unreleased] + +### Added + +- Formal gRPC read/watch and administrative publish/rollback services with Go and Python SDK transports. +- Audit console, user lifecycle management, RBAC enforcement, and immediate JWT invalidation through `token_version`. +- Release engineering checks for linting, vulnerability scanning, manifest validation, SBOM generation, and keyless image signing. + +### Changed + +- Runtime watch cursors now use etcd MVCC revisions with compaction-aware full synchronization. +- Redis was removed from the deployment architecture because it had no active runtime responsibility. diff --git a/Dockerfile b/Dockerfile index 5b2ebac..bb679b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,25 @@ -FROM golang:1.24-alpine AS build +FROM golang:1.25-alpine AS build WORKDIR /src +ARG VERSION=dev +ARG VCS_REF=unknown +ARG BUILD_DATE=unknown COPY go.mod go.sum ./ RUN go mod download COPY cmd ./cmd COPY internal ./internal COPY pkg ./pkg -RUN CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags="-s -w" -o /out/configcenter ./cmd/server +RUN CGO_ENABLED=0 go build -buildvcs=false -trimpath \ + -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${VCS_REF} -X main.buildDate=${BUILD_DATE}" \ + -o /out/configcenter ./cmd/server FROM alpine:3.21 +ARG VERSION=dev +ARG VCS_REF=unknown +ARG BUILD_DATE=unknown +LABEL org.opencontainers.image.title="Config Center Server" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.created="${BUILD_DATE}" RUN apk add --no-cache ca-certificates && adduser -D -u 10001 configcenter USER configcenter COPY --from=build /out/configcenter /usr/local/bin/configcenter diff --git a/Makefile b/Makefile index 6ea5cf0..a497376 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,17 @@ -.PHONY: dev test build proto proto-go proto-python proto-tools proto-check web-install web-build compose-up compose-down integration-smoke loadtest +.PHONY: dev test race build lint lint-tools vulncheck proto proto-go proto-python proto-tools proto-check yaml-tools validate-yaml validate-manifests python-check web-install web-build verify compose-up compose-down integration-smoke loadtest PROTOC_GEN_GO_VERSION := v1.36.5 PROTOC_GEN_GO_GRPC_VERSION := v1.5.1 GRPCIO_TOOLS_VERSION := 1.71.0 +GOLANGCI_LINT_VERSION := v2.12.2 +GOVULNCHECK_VERSION := v1.7.0 +YAMLLINT_VERSION := 1.37.1 PROTO_FILE := api/proto/configcenter/v1/config.proto PROTO_GO_FILES := pkg/proto/v1/config.pb.go pkg/proto/v1/config_grpc.pb.go PROTO_PY_FILES := sdk/python/configcenter/v1/config_pb2.py sdk/python/configcenter/v1/config_pb2_grpc.py PROTO_TOOLS_STAMP := .tools/.proto-tools-$(PROTOC_GEN_GO_VERSION)-$(PROTOC_GEN_GO_GRPC_VERSION)-$(GRPCIO_TOOLS_VERSION) +LINT_TOOLS_STAMP := .tools/.lint-tools-$(GOLANGCI_LINT_VERSION)-$(GOVULNCHECK_VERSION) +YAML_TOOLS_STAMP := .tools/.yaml-tools-$(YAMLLINT_VERSION) dev: go run ./cmd/server @@ -14,10 +19,27 @@ dev: test: GOCACHE=/tmp/configcenter-go-cache go test ./... +race: + GOCACHE=/tmp/configcenter-go-cache go test -race ./... + build: mkdir -p bin GOCACHE=/tmp/configcenter-go-cache go build -buildvcs=false -trimpath -o bin/configcenter ./cmd/server +lint-tools: $(LINT_TOOLS_STAMP) + +$(LINT_TOOLS_STAMP): + mkdir -p .tools/bin + GOBIN=$(CURDIR)/.tools/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + GOBIN=$(CURDIR)/.tools/bin go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) + touch $(LINT_TOOLS_STAMP) + +lint: lint-tools + $(CURDIR)/.tools/bin/golangci-lint run ./... + +vulncheck: lint-tools + $(CURDIR)/.tools/bin/govulncheck ./... + proto-tools: $(PROTO_TOOLS_STAMP) $(PROTO_TOOLS_STAMP): @@ -44,12 +66,32 @@ proto: proto-go proto-python proto-check: proto git diff --exit-code -- $(PROTO_FILE) $(PROTO_GO_FILES) $(PROTO_PY_FILES) sdk/python/configcenter/v1/__init__.py -web-install: - npm --prefix web install +yaml-tools: $(YAML_TOOLS_STAMP) -web-build: +$(YAML_TOOLS_STAMP): + rm -rf .tools/yamllint + mkdir -p .tools/yamllint + python3 -m pip install --disable-pip-version-check --target .tools/yamllint yamllint==$(YAMLLINT_VERSION) + touch $(YAML_TOOLS_STAMP) + +validate-yaml: yaml-tools + PYTHONPATH="$(CURDIR)/.tools/yamllint" python3 -m yamllint -c .yamllint.yml compose.yaml deploy .github/workflows + +validate-manifests: validate-yaml + docker compose config --quiet + sh scripts/check-migrations.sh + +python-check: proto-tools + PYTHONPATH="$(CURDIR)/.tools/python:$(CURDIR)/sdk/python" python3 -m compileall -q sdk/python/configcenter + +web-install: + npm --prefix web ci + +web-build: web-install npm --prefix web run build +verify: proto-check test race lint vulncheck python-check web-build validate-manifests + compose-up: docker compose up --build diff --git a/README.md b/README.md index 4d54dbc..6be8b1d 100644 --- a/README.md +++ b/README.md @@ -201,16 +201,14 @@ client.start_background_watch("application") ## 开发与验证 ```bash -make test -make proto-check -go test -race ./... +make verify make build -make web-build -docker compose config make integration-smoke make loadtest ``` +`make verify` 会执行 protobuf 生成代码漂移检查、Go 单元/集成测试与 race、`golangci-lint`、`govulncheck`、Python SDK 编译检查、Web Console 生产构建、YAML/Compose 校验和数据库 migration 规则检查。CI 另外使用 Trivy 对仓库依赖进行 HIGH/CRITICAL 漏洞阻断。 + 核心端到端测试覆盖:CRUD → 发布事务 → outbox worker → 运行时读取 → 灰度覆盖与指标;单元/HTTP 集成测试还覆盖 JWT 过期与篡改、应用级 RBAC、多版本回滚和 SDK 鉴权/灰度参数。 PostgreSQL 用户生命周期与审计集成测试在设置 `TEST_DATABASE_URL` 时执行;CI 会启动 PostgreSQL 16 service 自动运行该测试,覆盖 migration、用户禁用/密码重置/角色变化的 token version 轮换、最后管理员保护和审计游标分页。 @@ -223,6 +221,20 @@ go run ./cmd/loadtest \ -token="$CONFIGCENTER_TOKEN" -concurrency=64 -duration=60s -max-p95=200ms ``` +## 发布 + +正式版本使用 `vMAJOR.MINOR.PATCH` Git tag。创建 tag 前必须把 `CHANGELOG.md` 中对应内容整理到 `## [MAJOR.MINOR.PATCH] - YYYY-MM-DD`,否则 Release workflow 会拒绝发布。完整规则见 `docs/release-policy.md`。 + +tag 发布流程会构建并推送 server/web 的 `linux/amd64`、`linux/arm64` 镜像到 GHCR,对最终镜像执行 HIGH/CRITICAL 漏洞扫描,生成 SPDX JSON SBOM,并通过 GitHub OIDC 使用 Cosign 对不可变 digest 做 keyless 签名和即时验签。生产部署应引用已经过预生产验证的镜像 digest,不使用可变 tag。 + +服务二进制支持输出构建追溯信息: + +```bash +configcenter -version +``` + +版本号、Git commit 和构建时间同时写入 server/web OCI image labels。 + ## 生产部署 `deploy/kubernetes/configcenter.yaml` 提供三副本无状态 Config Server、三节点 etcd、跨节点调度、PDB、HPA、探针、资源限制和 etcd mTLS。`operations.yaml` 额外提供 etcd 访问网络策略、每 6 小时快照、每周 defrag 和每日 PostgreSQL 备份。部署前替换镜像与 Origin,并创建密钥: diff --git a/REMAINING_TASKS.md b/REMAINING_TASKS.md index a8eea3b..a980097 100644 --- a/REMAINING_TASKS.md +++ b/REMAINING_TASKS.md @@ -48,12 +48,13 @@ ## P3:发布工程化 -- [ ] CI 执行 Go 单元测试与 race、golangci-lint、前端构建、YAML/Compose 校验和生成代码漂移检查。 -- [ ] 构建并签名多架构镜像,生成 SBOM,执行依赖和镜像漏洞扫描。 -- [ ] 建立版本号、变更日志、数据库 migration 发布规则及回滚准入检查。 +- [x] CI 执行 Go 单元测试与 race、golangci-lint、`govulncheck`、前端构建、YAML/Compose 校验、migration 规则检查和生成代码漂移检查。 +- [x] 增加 tag Release workflow:构建并推送 server/web 的 `linux/amd64`、`linux/arm64` 镜像,生成 SPDX SBOM,执行依赖/最终镜像 HIGH/CRITICAL 漏洞阻断,并使用 GitHub OIDC + Cosign 对镜像 digest 做 keyless 签名和验签。 +- [x] 建立 SemVer tag、`CHANGELOG.md`、构建版本追溯、数据库 migration append-only/expand-migrate-contract 规则及应用回滚准入检查,见 `docs/release-policy.md`。 +- [ ] 在首个正式 `vMAJOR.MINOR.PATCH` tag 上验证 GHCR push、SBOM artifact、Cosign OIDC 签名/验签的真实 GitHub 执行结果,并记录最终 image digest。 - [ ] 在预生产执行持续压测和故障注入,验证 p95、错误率、Outbox 积压和 Watch 重连告警。 -完成标准:主分支 CI 全部通过;发布产物可追溯、可验证、可回滚;预生产持续压测和故障注入达到既定 SLO;发布前无未处理的高危依赖或镜像漏洞。 +仓库侧发布工程已完成;本地 server/web 最终镜像 HIGH/CRITICAL 扫描均为 0。完成标准仍要求首个真实 tag 的远端制品链验证,以及预生产持续压测/故障注入达到既定 SLO。 ## 推荐实施顺序 @@ -61,5 +62,5 @@ 2. ~~gRPC 代码生成、服务端和 SDK transport。~~ 已完成。 3. ~~审计控制台与用户生命周期,并明确 JWT 即时失效策略。~~ 已完成。 4. ~~Redis ADR 和清理。~~ 已完成,结论为移除 Redis。 -5. CI/CD、制品签名、SBOM、漏洞扫描和发布规则。 +5. ~~CI/CD、制品签名、SBOM、漏洞扫描和发布规则。~~ 仓库实现完成,待首个真实 tag 验证远端制品链。 6. 预生产 mTLS、备份恢复、故障演练与生产准入验收。 diff --git a/cmd/server/main.go b/cmd/server/main.go index 94e1acf..54f4904 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -33,11 +33,23 @@ import ( "github.com/longpeng/configcenter/internal/watch" ) +var ( + version = "dev" + commit = "unknown" + buildDate = "unknown" +) + func main() { migrateOnly := flag.Bool("migrate-only", false, "apply database migrations and exit") + showVersion := flag.Bool("version", false, "print build version and exit") flag.Parse() + if *showVersion { + fmt.Printf("configcenter %s commit=%s built=%s\n", version, commit, buildDate) + return + } logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + logger.Info("starting config center", "version", version, "commit", commit, "buildDate", buildDate) cfg := config.Load() ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() @@ -93,7 +105,7 @@ func main() { healthServer := health.NewServer() grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) - grpcListener, err := net.Listen("tcp", cfg.GRPCAddr) + grpcListener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", cfg.GRPCAddr) if err != nil { logger.Error("listen grpc", "address", cfg.GRPCAddr, "error", err) os.Exit(1) @@ -113,7 +125,7 @@ func main() { case <-ctx.Done(): logger.Info("shutting down config center") case err := <-serverErrors: - if err != nil && err != http.ErrServerClosed && !errors.Is(err, grpc.ErrServerStopped) { + if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, grpc.ErrServerStopped) { logger.Error("server stopped", "error", err) } } diff --git a/docs/release-policy.md b/docs/release-policy.md new file mode 100644 index 0000000..697db76 --- /dev/null +++ b/docs/release-policy.md @@ -0,0 +1,49 @@ +# Release Policy + +## Version source of truth + +Config Center uses Semantic Versioning. A production release is created only from an annotated or lightweight Git tag named `vMAJOR.MINOR.PATCH`, for example `v0.2.0`. + +Before creating the tag, move the relevant entries from `CHANGELOG.md` `Unreleased` into a dated heading in the form `## [MAJOR.MINOR.PATCH] - YYYY-MM-DD`. The release workflow rejects a tag that has no matching changelog heading. + +Server images expose the release version, Git commit, and build timestamp through OCI labels and the `configcenter -version` command. Production deployments must reference immutable image digests rather than mutable tags. + +## Required release gates + +A release is eligible only when all repository verification succeeds: + +1. protobuf generation has no drift; +2. Go unit/integration tests and race tests pass; +3. `golangci-lint` and `govulncheck` pass; +4. Python SDK compile validation and Web Console production build pass; +5. YAML, Docker Compose, and migration policy validation pass; +6. dependency and final container image scans contain no untreated HIGH or CRITICAL vulnerabilities. + +The tag-triggered workflow then builds Linux `amd64` and `arm64` server/web images, pushes them to GHCR, generates SPDX JSON SBOMs, and signs each immutable digest with Cosign using GitHub OIDC. The workflow verifies the signature immediately after signing. + +## Database migration rules + +PostgreSQL migrations under `internal/store/postgres/migrations` are append-only and use contiguous filenames such as `004_add_feature.sql`. + +- Never edit, rename, reorder, or delete a migration that may have been applied outside a developer's disposable database. +- Schema changes must be forward-compatible with the previous application release during rolling deployment. +- Destructive changes use expand/migrate/contract: add the new structure first, migrate traffic/data, and remove the old structure only in a later release. +- The application runtime verifies migration checksums and refuses to start if an already-recorded migration has changed. + +## Rollback admission + +Application rollback is permitted only when the database schema currently deployed remains compatible with the target application version. If a release contains a migration that breaks backward compatibility, rollback is blocked until a corrective forward migration restores compatibility. + +Database rollback is not implemented by executing down migrations in production. Recovery from an erroneous destructive migration uses a tested PostgreSQL backup/restore procedure or a new forward migration, depending on incident scope. + +## Release procedure + +1. Ensure the main branch is green and update `CHANGELOG.md`. +2. Run `make verify` locally where practical. +3. Create and push `vMAJOR.MINOR.PATCH`. +4. Wait for the `Release` workflow to finish successfully. +5. Record the server and web image digests and deploy by digest to staging. +6. Complete staging smoke/load/fault checks and production change approval. +7. Promote the exact tested digests to production; do not rebuild between staging and production. + +The remaining staging mTLS, backup/restore, sustained load, and fault-injection gates are tracked separately because they require a real multi-node environment. diff --git a/go.mod b/go.mod index e4330e5..dcce8d3 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/longpeng/configcenter -go 1.24.0 +go 1.25.0 require ( - github.com/jackc/pgx/v5 v5.7.2 + github.com/jackc/pgx/v5 v5.10.0 go.etcd.io/etcd/api/v3 v3.6.7 go.etcd.io/etcd/client/v3 v3.6.7 - golang.org/x/crypto v0.42.0 - google.golang.org/grpc v1.71.1 - google.golang.org/protobuf v1.36.5 + golang.org/x/crypto v0.53.0 + google.golang.org/grpc v1.83.0 + google.golang.org/protobuf v1.36.11 ) require ( @@ -23,10 +23,10 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.45.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.41.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect ) diff --git a/go.sum b/go.sum index 9eae92b..e94a366 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -5,8 +7,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -24,8 +26,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -35,8 +37,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0= @@ -45,18 +47,18 @@ go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/Ah go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q= go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U= go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -66,30 +68,30 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -98,14 +100,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= -google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/api/httpapi/server_test.go b/internal/api/httpapi/server_test.go index fcfb7e8..564dcfe 100644 --- a/internal/api/httpapi/server_test.go +++ b/internal/api/httpapi/server_test.go @@ -80,7 +80,7 @@ func TestCRUDPublishAndRuntimeRead(t *testing.T) { streamCtx, streamCancel := context.WithCancel(ctx) defer streamCancel() - streamRequest := httptest.NewRequest(http.MethodGet, "/v1/watch?env=PROD&app=orders&namespace=application&instance=orders-7", nil).WithContext(streamCtx) + streamRequest := httptest.NewRequestWithContext(streamCtx, http.MethodGet, "/v1/watch?env=PROD&app=orders&namespace=application&instance=orders-7", nil) streamRecorder := newStreamResponseRecorder() streamDone := make(chan struct{}) go func() { @@ -147,7 +147,7 @@ func TestAuthenticationRBACAndMetrics(t *testing.T) { collector := metrics.New() handler := httpapi.New(repository, runtimeStore, watch.New(ctx, runtimeStore), authorizer, collector, nil, logger).Handler() - request := httptest.NewRequest(http.MethodGet, "/v1/applications", nil) + request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/applications", nil) doStatus(t, handler, request, http.StatusUnauthorized) var adminLogin struct { @@ -190,9 +190,9 @@ func TestAuthenticationRBACAndMetrics(t *testing.T) { } requestJSON(t, handler, http.MethodPost, "/v1/namespaces", viewerLogin.Token, map[string]any{"appId": application.ID, "name": "application", "type": "properties"}, http.StatusForbidden, nil) - doStatus(t, handler, httptest.NewRequest(http.MethodGet, "/health/live", nil), http.StatusOK) + doStatus(t, handler, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/health/live", nil), http.StatusOK) recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/metrics", nil)) + handler.ServeHTTP(recorder, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/metrics", nil)) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `configcenter_http_requests_total{method="GET",route="GET /health/live",status="200"}`) || !strings.Contains(recorder.Body.String(), "configcenter_outbox_pending 0") { t.Fatalf("unexpected metrics response: status=%d body=%s", recorder.Code, recorder.Body.String()) } @@ -291,7 +291,7 @@ func post(t *testing.T, handler http.Handler, path string, input any, wantStatus if err != nil { t.Fatal(err) } - request := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(payload)) + request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, path, bytes.NewReader(payload)) request.Header.Set("Content-Type", "application/json") request.Header.Set("X-User", "tester") do(t, handler, request, wantStatus, output) @@ -299,7 +299,7 @@ func post(t *testing.T, handler http.Handler, path string, input any, wantStatus func get(t *testing.T, handler http.Handler, path string, wantStatus int, output any) { t.Helper() - request := httptest.NewRequest(http.MethodGet, path, nil) + request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, path, nil) do(t, handler, request, wantStatus, output) } @@ -337,7 +337,7 @@ func requestJSON(t *testing.T, handler http.Handler, method, path, token string, } body = bytes.NewReader(payload) } - request := httptest.NewRequest(method, path, body) + request := httptest.NewRequestWithContext(context.Background(), method, path, body) if input != nil { request.Header.Set("Content-Type", "application/json") } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 5d259da..29b4cda 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -27,7 +27,7 @@ func TestLoginTokenExpiryAndApplicationRoles(t *testing.T) { if err != nil || !principal.IsAdmin { t.Fatalf("bootstrap login failed: principal=%#v err=%v", principal, err) } - request := httptest.NewRequest("GET", "/v1/applications", nil) + request := httptest.NewRequestWithContext(ctx, "GET", "/v1/applications", nil) request.Header.Set("Authorization", "Bearer "+token) parsed, err := manager.AuthenticateRequest(request) if err != nil || parsed.Username != "root" { @@ -64,7 +64,7 @@ func TestLoginTokenExpiryAndApplicationRoles(t *testing.T) { if _, err := manager.AuthenticateRequest(request); !errors.Is(err, ErrUnauthorized) { t.Fatalf("expired token must be rejected: %v", err) } - tampered := httptest.NewRequest("GET", "/", nil) + tampered := httptest.NewRequestWithContext(ctx, "GET", "/", nil) tampered.Header.Set("Authorization", "Bearer "+token+"x") if _, err := manager.AuthenticateRequest(tampered); !errors.Is(err, ErrUnauthorized) { t.Fatalf("tampered token must be rejected: %v", err) diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go index 7e52d5b..da8621c 100644 --- a/internal/metrics/metrics_test.go +++ b/internal/metrics/metrics_test.go @@ -1,6 +1,7 @@ package metrics_test import ( + "context" "net/http" "net/http/httptest" "strings" @@ -17,7 +18,7 @@ func TestHTTPDurationHistogram(t *testing.T) { collector.ObserveHTTP(http.MethodGet, "GET /v1/config", http.StatusOK, 300*time.Millisecond) recorder := httptest.NewRecorder() - collector.Handler(memory_store.New(false)).ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/metrics", nil)) + collector.Handler(memory_store.New(false)).ServeHTTP(recorder, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/metrics", nil)) if recorder.Code != http.StatusOK { t.Fatalf("unexpected metrics status: %d", recorder.Code) } diff --git a/pkg/sdk/go/client.go b/pkg/sdk/go/client.go index c69f4eb..a05fdd7 100644 --- a/pkg/sdk/go/client.go +++ b/pkg/sdk/go/client.go @@ -225,7 +225,7 @@ func (c *Client) WatchAndSync(ctx context.Context, namespace string) error { for { err := c.watchOnce(ctx, namespace) if ctx.Err() != nil { - return nil + return ctx.Err() } if err == nil { backoff = time.Second diff --git a/scripts/check-migrations.sh b/scripts/check-migrations.sh new file mode 100644 index 0000000..6f96119 --- /dev/null +++ b/scripts/check-migrations.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env sh +set -eu + +migration_dir="internal/store/postgres/migrations" +previous=0 + +for path in "$migration_dir"/*.sql; do + name="$(basename "$path")" + case "$name" in + [0-9][0-9][0-9]_[a-z0-9_]*.sql) ;; + *) + printf 'invalid migration filename: %s\n' "$name" >&2 + exit 1 + ;; + esac + number="${name%%_*}" + number="$(printf '%s' "$number" | sed 's/^0*//')" + [ -n "$number" ] || number=0 + if [ "$number" -ne $((previous + 1)) ]; then + printf 'migration sequence must be contiguous: expected %03d, found %03d (%s)\n' "$((previous + 1))" "$number" "$name" >&2 + exit 1 + fi + previous="$number" +done + +base_ref="${MIGRATION_BASE_REF:-}" +if [ -n "$base_ref" ] && git cat-file -e "$base_ref^{commit}" 2>/dev/null; then + changed="$(git diff --name-status "$base_ref"...HEAD -- "$migration_dir" || true)" + if [ -n "$changed" ]; then + printf '%s\n' "$changed" | while IFS="$(printf '\t')" read -r status path rest; do + case "$status" in + A) ;; + *) + printf 'existing migrations are immutable; only new migration files may be added: %s %s %s\n' "$status" "$path" "$rest" >&2 + exit 1 + ;; + esac + done + fi +fi + +printf 'migration policy check passed (%d migrations)\n' "$previous" diff --git a/scripts/check-release.sh b/scripts/check-release.sh new file mode 100644 index 0000000..1adad5a --- /dev/null +++ b/scripts/check-release.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +set -eu + +tag="${1:-}" +if ! printf '%s\n' "$tag" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then + printf 'release tag must use vMAJOR.MINOR.PATCH, got: %s\n' "$tag" >&2 + exit 1 +fi + +version="${tag#v}" +if ! grep -Eq "^## \\[$version\\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md; then + printf 'CHANGELOG.md must contain a dated release heading: ## [%s] - YYYY-MM-DD\n' "$version" >&2 + exit 1 +fi + +sh scripts/check-migrations.sh +printf 'release policy check passed for %s\n' "$tag" diff --git a/web/Dockerfile b/web/Dockerfile index 4e5f74f..054682e 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -6,7 +6,15 @@ COPY ConfigCenter.jsx ./ConfigCenter.jsx COPY web ./web RUN cd web && npm run build -FROM nginx:1.27-alpine +FROM nginx:1.31.4-alpine3.24-slim +ARG VERSION=dev +ARG VCS_REF=unknown +ARG BUILD_DATE=unknown +LABEL org.opencontainers.image.title="Config Center Web" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + org.opencontainers.image.created="${BUILD_DATE}" +RUN apk upgrade --no-cache COPY web/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /src/web/dist /usr/share/nginx/html EXPOSE 80