feat: add release engineering pipeline
This commit is contained in:
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
178
.github/workflows/release.yml
vendored
Normal file
178
.github/workflows/release.yml
vendored
Normal file
@@ -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}"
|
||||
18
.golangci.yml
Normal file
18
.golangci.yml
Normal file
@@ -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
|
||||
8
.yamllint.yml
Normal file
8
.yamllint.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
braces: disable
|
||||
document-start: disable
|
||||
line-length: disable
|
||||
truthy:
|
||||
allowed-values: ["true", "false", "on"]
|
||||
16
CHANGELOG.md
Normal file
16
CHANGELOG.md
Normal file
@@ -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.
|
||||
16
Dockerfile
16
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
|
||||
|
||||
50
Makefile
50
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
|
||||
|
||||
|
||||
22
README.md
22
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,并创建密钥:
|
||||
|
||||
@@ -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、备份恢复、故障演练与生产准入验收。
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
49
docs/release-policy.md
Normal file
49
docs/release-policy.md
Normal file
@@ -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.
|
||||
22
go.mod
22
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
|
||||
)
|
||||
|
||||
76
go.sum
76
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=
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
42
scripts/check-migrations.sh
Normal file
42
scripts/check-migrations.sh
Normal file
@@ -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"
|
||||
17
scripts/check-release.sh
Normal file
17
scripts/check-release.sh
Normal file
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user