feat: add release identity manifest
This commit is contained in:
5
Makefile
5
Makefile
@@ -1,6 +1,6 @@
|
||||
GO ?= go
|
||||
|
||||
.PHONY: fmt test vet build image integration mysql-replication mysql-compatibility sdk-check protocol-check docs-check schedule-backlog release-local-check
|
||||
.PHONY: fmt test vet build image integration mysql-replication mysql-compatibility sdk-check protocol-check docs-check schedule-backlog release-manifest release-local-check
|
||||
fmt:
|
||||
$(GO) fmt ./...
|
||||
cd sdk/go && $(GO) fmt ./...
|
||||
@@ -47,6 +47,9 @@ protocol-check:
|
||||
docs-check:
|
||||
./scripts/check-doc-links.sh
|
||||
|
||||
release-manifest:
|
||||
$(GO) run ./cmd/orbit-admin release manifest
|
||||
|
||||
release-local-check:
|
||||
./scripts/check-release-local.sh
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/config"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
@@ -23,9 +24,11 @@ func main() {
|
||||
}
|
||||
func run() error {
|
||||
if len(os.Args) < 2 {
|
||||
return errors.New("usage: orbit-admin bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
||||
return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "release":
|
||||
return runRelease(os.Args[2:])
|
||||
case "bootstrap":
|
||||
return runBootstrap(os.Args[2:])
|
||||
case "verify":
|
||||
@@ -37,7 +40,57 @@ func run() error {
|
||||
case "credential":
|
||||
return runCredential(os.Args[2:])
|
||||
}
|
||||
return errors.New("usage: orbit-admin bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
||||
return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
||||
}
|
||||
|
||||
// releaseManifest 是发布物自身的静态身份清单,不读取数据库、不包含 DSN、凭据或业务数据。
|
||||
// 部署前可以保存它,与镜像摘要、git 提交和目标数据库的 verify 输出一起作为发布证据。
|
||||
type releaseManifest struct {
|
||||
ManifestVersion int `json:"manifest_version"` // ManifestVersion 清单格式版本,变更时递增。
|
||||
Build buildinfo.Info `json:"build"` // Build 当前二进制及协议身份。
|
||||
Schema releaseSchemaManifest `json:"schema"` // Schema 当前内置迁移链摘要。
|
||||
}
|
||||
|
||||
// releaseSchemaManifest 描述当前二进制可执行的前向迁移链;SQL 正文不写入清单。
|
||||
type releaseSchemaManifest struct {
|
||||
CurrentVersion uint64 `json:"current_version"` // CurrentVersion 最新迁移版本。
|
||||
MigrationCount int `json:"migration_count"` // MigrationCount 迁移数量。
|
||||
Migrations []releaseMigrationManifest `json:"migrations"` // Migrations 按版本排序的名称和校验和。
|
||||
}
|
||||
|
||||
// releaseMigrationManifest 是单条迁移的不可变身份,不暴露迁移 SQL。
|
||||
type releaseMigrationManifest struct {
|
||||
Version uint64 `json:"version"` // Version 迁移版本。
|
||||
Name string `json:"name"` // Name 迁移文件名。
|
||||
Checksum string `json:"checksum"` // Checksum 迁移内容 SHA-256。
|
||||
}
|
||||
|
||||
func runRelease(args []string) error {
|
||||
if len(args) != 1 || args[0] != "manifest" {
|
||||
return errors.New("usage: orbit-admin release manifest")
|
||||
}
|
||||
migrations, err := mysql.Migrations()
|
||||
if err != nil || len(migrations) == 0 {
|
||||
return errors.New("release migration manifest unavailable")
|
||||
}
|
||||
items := make([]releaseMigrationManifest, 0, len(migrations))
|
||||
for _, migration := range migrations {
|
||||
items = append(items, releaseMigrationManifest{
|
||||
Version: migration.Version,
|
||||
Name: migration.Name,
|
||||
Checksum: migration.Checksum,
|
||||
})
|
||||
}
|
||||
manifest := releaseManifest{
|
||||
ManifestVersion: 1,
|
||||
Build: buildinfo.Current(),
|
||||
Schema: releaseSchemaManifest{
|
||||
CurrentVersion: migrations[len(migrations)-1].Version,
|
||||
MigrationCount: len(items),
|
||||
Migrations: items,
|
||||
},
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(manifest)
|
||||
}
|
||||
|
||||
func runVerify(args []string) error {
|
||||
|
||||
74
cmd/orbit-admin/release_test.go
Normal file
74
cmd/orbit-admin/release_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||
)
|
||||
|
||||
func TestReleaseManifestIsDatabaseIndependent(t *testing.T) {
|
||||
output, restore := captureStdout(t)
|
||||
err := runRelease([]string{"manifest"})
|
||||
restore()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got releaseManifest
|
||||
if err := json.Unmarshal(output.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
migrations, err := mysql.Migrations()
|
||||
if err != nil || len(migrations) == 0 {
|
||||
t.Fatalf("migrations: %v", err)
|
||||
}
|
||||
if got.ManifestVersion != 1 {
|
||||
t.Fatalf("manifest version=%d", got.ManifestVersion)
|
||||
}
|
||||
if got.Build != buildinfo.Current() {
|
||||
t.Fatalf("build identity=%+v want=%+v", got.Build, buildinfo.Current())
|
||||
}
|
||||
if got.Schema.CurrentVersion != migrations[len(migrations)-1].Version || got.Schema.MigrationCount != len(migrations) {
|
||||
t.Fatalf("schema summary=%+v migrations=%d", got.Schema, len(migrations))
|
||||
}
|
||||
if len(got.Schema.Migrations) != len(migrations) {
|
||||
t.Fatalf("manifest migrations=%d want=%d", len(got.Schema.Migrations), len(migrations))
|
||||
}
|
||||
for i, migration := range migrations {
|
||||
item := got.Schema.Migrations[i]
|
||||
if item.Version != migration.Version || item.Name != migration.Name || item.Checksum != migration.Checksum {
|
||||
t.Fatalf("migration[%d]=%+v want version=%d name=%s checksum=%s", i, item, migration.Version, migration.Name, migration.Checksum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseManifestRejectsUnknownCommand(t *testing.T) {
|
||||
if err := runRelease([]string{"status"}); err == nil {
|
||||
t.Fatal("unknown release command accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T) (*bytes.Buffer, func()) {
|
||||
t.Helper()
|
||||
old := os.Stdout
|
||||
read, write, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = write
|
||||
var output bytes.Buffer
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = output.ReadFrom(read)
|
||||
close(done)
|
||||
}()
|
||||
return &output, func() {
|
||||
_ = write.Close()
|
||||
os.Stdout = old
|
||||
<-done
|
||||
_ = read.Close()
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
| P4 Weave 主流程 | SDK 定义构建与发布校验、实例、顺序/分支/结构化并行汇合、信号/定时等待、版本绑定 | 重启接续、早到/重复信号、并行竞争、内部事件重放、发布版本兼容;端到端通用流程 | 已完成平台功能阶段验收:[原功能及W01–W17逐项核对](reports/phase-4-5.md)、[版本/暂停/全停回归](reports/phase-4-5-races.md)、[提交故障边界](reports/phase-6-fault-boundaries.md)。研发发布、固定版本、顺序/分支/有限并行、信号/时间等待、公共API/SDK及可运行示例均已交付;生产拓扑与发布兼容继续P6/P7 |
|
||||
| P5 Weave 恢复与查询 | 有界查询、暂停继续、取消、人工核查恢复、控制门控与审计 | 轮次与尝试独立预算;暂停/开始/回执/超时竞争;恢复保留预算;未知结果不盲目重复 | 已完成平台功能阶段验收:[原功能核对](reports/phase-4-5.md)。有界查询、独立预算、暂停继续/取消、六类恢复、历史/筛选/事件/来源与SDK场景均已交付;真实竞争、COMMIT断线及进程全停保留原身份、截止和预算。运维、容量与首版发布继续P6/P7 |
|
||||
| P6 生产验证与运维 | 完整监控、凭据轮换、备份恢复、滚动升级、运行手册、可复现负载和故障工具 | 对照生产验收清单执行,报告环境/指标/遗留项;容量、RPO/RTO 按真实拓扑验证 | 进行中;已补[精确故障边界](reports/phase-6-fault-boundaries.md)和[进程观测](reports/phase-6-observability.md);继续业务积压/阶段延迟、凭据轮换及其余生产门槛 |
|
||||
| P7 首版交付核对 | SDK 兼容与发布准备、完整 API 手册、场景示例、全量回归及交付清单 | 新接入者按文档跑通全部示例;所有首版能力可追溯到实现与测试;报告实际生产边界 | 进行中;已补[通用文件转换示例](reports/phase-7-file-conversion.md)、[SDK独立发布门禁](reports/phase-7-sdk-release.md)、[当前协议兼容矩阵](reports/phase-7-compatibility.md)、[历史任务 API 双向演练](reports/phase-7-historical-compatibility.md)、[当前回归状态](reports/phase-7-regression-status.md)、[首版交付核对清单](release-checklist.md)和[容器发布说明](21-container-deployment.md),镜像已完成 linux/amd64 构建验证,节点版本身份可由 `/version` 核对;仍需带标签的完整跨版本矩阵和目标拓扑验收 |
|
||||
| P7 首版交付核对 | SDK 兼容与发布准备、完整 API 手册、场景示例、全量回归及交付清单 | 新接入者按文档跑通全部示例;所有首版能力可追溯到实现与测试;报告实际生产边界 | 进行中;已补[通用文件转换示例](reports/phase-7-file-conversion.md)、[SDK独立发布门禁](reports/phase-7-sdk-release.md)、[当前协议兼容矩阵](reports/phase-7-compatibility.md)、[历史任务 API 双向演练](reports/phase-7-historical-compatibility.md)、[当前回归状态](reports/phase-7-regression-status.md)、[首版交付核对清单](release-checklist.md)、[发布物身份清单](api/operations.md#发布物身份清单)和[容器发布说明](21-container-deployment.md),镜像已完成 linux/amd64 构建验证,节点版本身份可由 `/version` 核对;仍需带标签的完整跨版本矩阵和目标拓扑验收 |
|
||||
|
||||
SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最后才开始补文档。
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ docker build --platform linux/amd64 \
|
||||
|
||||
镜像同时写入 OCI `org.opencontainers.image.version`、`org.opencontainers.image.revision` 和 `org.opencontainers.image.created` 标签;可以先核对标签,再在节点启动后通过 `GET /version` 核对进程实际使用的值。`registry.example` 只是占位符,不是平台依赖。
|
||||
|
||||
构建完成后先保存发布物身份清单:
|
||||
|
||||
```sh
|
||||
docker run --rm --entrypoint /usr/local/bin/orbit-admin \
|
||||
registry.example/orbit:1.0.0 release manifest > orbit-release-manifest.json
|
||||
```
|
||||
|
||||
清单把当前构建身份、协议版本和内置迁移校验和固定下来;部署前再用 `orbit-admin verify` 对目标数据库逐项核对。两者不一致时停止发布,不通过人工修改清单来“对齐”。
|
||||
|
||||
## 2. 迁移、核验和启动
|
||||
|
||||
迁移与服务使用同一镜像但分开执行;迁移账户和运行账户由数据库权限策略隔离。以下命令只展示容器契约,凭据必须来自秘密管理:
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
# 发布物身份清单
|
||||
|
||||
`orbit-admin release manifest` 不连接数据库,输出当前二进制内置的构建身份、协议版本和完整迁移名称/校验和。它用于在构建阶段冻结发布证据;输出不含 DSN、凭据、迁移 SQL 或业务数据。
|
||||
|
||||
```sh
|
||||
docker run --rm --entrypoint /usr/local/bin/orbit-admin registry.example/orbit:1.0.0 release manifest > orbit-release-manifest.json
|
||||
# 本地工作区也可执行:
|
||||
make release-manifest > orbit-release-manifest.json
|
||||
```
|
||||
|
||||
清单中的 `build.revision` 应与源码提交和镜像 OCI revision 标签一致,`schema` 应与部署前 `orbit-admin verify` 返回的迁移账本逐项一致。清单只证明发布物声明的身份,不能证明目标数据库已切换、备份可恢复或目标拓扑满足 RPO/RTO。
|
||||
|
||||
示例结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"build": {
|
||||
"version": "1.0.0",
|
||||
"revision": "<immutable-commit>",
|
||||
"built_at": "2026-09-20T00:00:00Z",
|
||||
"go_version": "go1.27.1",
|
||||
"http_protocol_version": "1",
|
||||
"worker_protocol_version": "1",
|
||||
"event_protocol_version": "1",
|
||||
"workflow_schema_version": 1
|
||||
},
|
||||
"schema": {
|
||||
"current_version": 28,
|
||||
"migration_count": 28,
|
||||
"migrations": [
|
||||
{"version": 1, "name": "000001_initial.up.sql", "checksum": "<sha256>"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 发布前结构核验
|
||||
|
||||
`orbit-admin verify` 是只读的发布、迁移和恢复前置检查。它不会执行迁移、修复脏账本、删除数据或修改权限;数据库账户通过 `ORBIT_MYSQL_DSN` 注入,建议使用受限只读账户。
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
| 服务端与 SDK 代码来自同一提交 | `git rev-parse HEAD`;发布时记录不可变提交 | 待发布候选冻结 |
|
||||
| Go 工具链 | Go 1.27.1;Docker `golang:1.27.1`,真实 `go.mod`/`GOWORK=off` | 模块级 race/vet/build、MySQL 存储/进程组、24 个 SDK 示例及两个独立业务模块真实集成已通过;发布环境仍需复核 |
|
||||
| 容器发布物 | `make image`;[容器发布说明](21-container-deployment.md) | native linux/arm64 与显式 linux/amd64 镜像构建已通过,支持注入版本身份并由 `/version` 核对;生产镜像标签、摘要、签名和目标编排环境仍需冻结 |
|
||||
| 发布物身份清单 | `orbit-admin release manifest`;[运维接口](api/operations.md) | 已实现无需数据库的机器可读清单,包含构建身份、协议版本和全部迁移校验和;正式发布仍需把清单、镜像摘要与目标库 `verify` 输出一并冻结 |
|
||||
| 服务端单元、竞态、vet、build | `make test && make vet && make build` | Go 1.27.1 Docker 等价命令已通过;宿主机 `make` 因未安装 Go 不能作为本机证据 |
|
||||
| 当前协议线一致 | `make protocol-check` | 已通过:HTTP `/v1`、Worker/Event v1、流程 schema 1 |
|
||||
| SDK 独立性和发布门禁 | `make sdk-check` | Go 1.27.1 Docker 下独立 module、示例和业务模块 race/vet/build 已通过 |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
| 检查 | 环境/命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 本地发布门禁统一入口 | 2026-09-20;`make release-local-check` | PASS;服务端、SDK、独立示例竞态测试、vet、build、当前协议门禁、146个 Markdown 本地链接和 SDK 独立依赖检查均通过;命令明确不替代真实 MySQL 集成与目标生产验收 |
|
||||
| 发布物静态身份清单 | 2026-09-20;`make release-manifest`;无 `ORBIT_MYSQL_DSN` | PASS;输出构建/协议身份及28条迁移名称与SHA-256校验和,未读取数据库、凭据或业务数据;目标库仍需单独执行 `orbit-admin verify` |
|
||||
| 就绪探针排空门控 | `go test -race ./internal/transport/httpserver ./cmd/orbit`;`TestReadinessEntersDrainBeforeShutdown` | PASS;SIGTERM 关闭路径先将 `/health/ready` 置为 503,再进入限定优雅关闭窗口;响应不泄露内部错误 |
|
||||
| 健康探针响应契约 | 2026-09-20;`go test ./internal/transport/httpserver && go test -race ./internal/transport/httpserver && make docs-check` | PASS;`/health/live` 与 `/health/ready` 返回 `Cache-Control: no-store`、`X-Content-Type-Options: nosniff`;未就绪和排空状态返回 `Retry-After: 1`,并由 HTTP 契约测试覆盖 |
|
||||
| 数据模型中文注释门禁 | 2026-09-20;`go test ./internal/storage/mysql -run 'Test(FreshInstallBaselineHasChineseModelComments|FreshInstallBaselineContainsNoReferenceDDL|MigrationsAfterForeignKeyRemovalDoNotReintroduceReferences|PostRemovalMigrationGuard)$'` | PASS;当前无外键基线逐表、逐持久化字段检查中文 `COMMENT`,并继续阻止后续迁移重新引入引用约束 |
|
||||
|
||||
Reference in New Issue
Block a user