feat: add release identity manifest

This commit is contained in:
2026-09-20 21:18:09 +08:00
parent 014591560a
commit cebb32138c
8 changed files with 182 additions and 4 deletions

View File

@@ -1,6 +1,6 @@
GO ?= go 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: fmt:
$(GO) fmt ./... $(GO) fmt ./...
cd sdk/go && $(GO) fmt ./... cd sdk/go && $(GO) fmt ./...
@@ -47,6 +47,9 @@ protocol-check:
docs-check: docs-check:
./scripts/check-doc-links.sh ./scripts/check-doc-links.sh
release-manifest:
$(GO) run ./cmd/orbit-admin release manifest
release-local-check: release-local-check:
./scripts/check-release-local.sh ./scripts/check-release-local.sh

View File

@@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
"git.yuntuo.hk/k2safe/orbit/internal/config" "git.yuntuo.hk/k2safe/orbit/internal/config"
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql" "git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
core "git.yuntuo.hk/k2safe/orbit/model" core "git.yuntuo.hk/k2safe/orbit/model"
@@ -23,9 +24,11 @@ func main() {
} }
func run() error { func run() error {
if len(os.Args) < 2 { 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] { switch os.Args[1] {
case "release":
return runRelease(os.Args[2:])
case "bootstrap": case "bootstrap":
return runBootstrap(os.Args[2:]) return runBootstrap(os.Args[2:])
case "verify": case "verify":
@@ -37,7 +40,57 @@ func run() error {
case "credential": case "credential":
return runCredential(os.Args[2:]) 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 { func runVerify(args []string) error {

View 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()
}
}

View File

@@ -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 | | 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 | | 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);继续业务积压/阶段延迟、凭据轮换及其余生产门槛 | | 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 是全量核对,不是最后才开始补文档。 SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最后才开始补文档。

View File

@@ -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` 只是占位符,不是平台依赖。 镜像同时写入 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. 迁移、核验和启动 ## 2. 迁移、核验和启动
迁移与服务使用同一镜像但分开执行;迁移账户和运行账户由数据库权限策略隔离。以下命令只展示容器契约,凭据必须来自秘密管理: 迁移与服务使用同一镜像但分开执行;迁移账户和运行账户由数据库权限策略隔离。以下命令只展示容器契约,凭据必须来自秘密管理:

View File

@@ -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` 注入,建议使用受限只读账户。 `orbit-admin verify` 是只读的发布、迁移和恢复前置检查。它不会执行迁移、修复脏账本、删除数据或修改权限;数据库账户通过 `ORBIT_MYSQL_DSN` 注入,建议使用受限只读账户。

View File

@@ -9,6 +9,7 @@
| 服务端与 SDK 代码来自同一提交 | `git rev-parse HEAD`;发布时记录不可变提交 | 待发布候选冻结 | | 服务端与 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 示例及两个独立业务模块真实集成已通过;发布环境仍需复核 | | 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` 核对;生产镜像标签、摘要、签名和目标编排环境仍需冻结 | | 容器发布物 | `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 不能作为本机证据 | | 服务端单元、竞态、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 | | 当前协议线一致 | `make protocol-check` | 已通过:HTTP `/v1`、Worker/Event v1、流程 schema 1 |
| SDK 独立性和发布门禁 | `make sdk-check` | Go 1.27.1 Docker 下独立 module、示例和业务模块 race/vet/build 已通过 | | SDK 独立性和发布门禁 | `make sdk-check` | Go 1.27.1 Docker 下独立 module、示例和业务模块 race/vet/build 已通过 |

View File

@@ -7,6 +7,7 @@
| 检查 | 环境/命令 | 结果 | | 检查 | 环境/命令 | 结果 |
| --- | --- | --- | | --- | --- | --- |
| 本地发布门禁统一入口 | 2026-09-20;`make release-local-check` | PASS;服务端、SDK、独立示例竞态测试、vet、build、当前协议门禁、146个 Markdown 本地链接和 SDK 独立依赖检查均通过;命令明确不替代真实 MySQL 集成与目标生产验收 | | 本地发布门禁统一入口 | 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,再进入限定优雅关闭窗口;响应不泄露内部错误 | | 就绪探针排空门控 | `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/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`,并继续阻止后续迁移重新引入引用约束 | | 数据模型中文注释门禁 | 2026-09-20;`go test ./internal/storage/mysql -run 'Test(FreshInstallBaselineHasChineseModelComments|FreshInstallBaselineContainsNoReferenceDDL|MigrationsAfterForeignKeyRemovalDoNotReintroduceReferences|PostRemovalMigrationGuard)$'` | PASS;当前无外键基线逐表、逐持久化字段检查中文 `COMMENT`,并继续阻止后续迁移重新引入引用约束 |