feat: gate releases on platform health

This commit is contained in:
Codex Local
2026-08-26 22:04:38 +08:00
parent 47ef596d3b
commit e4f7701439
12 changed files with 189 additions and 33 deletions
+92 -18
View File
@@ -3,6 +3,8 @@ package diagnostics
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strings"
@@ -69,6 +71,17 @@ type AcceptanceAlertState struct {
BlockingItemsTruncated bool `json:"blocking_items_truncated"`
}
type AcceptancePlatformHealthState struct {
State string `json:"state"`
Status string `json:"status,omitempty"`
ObservedAt *time.Time `json:"observed_at,omitempty"`
ReceivedAt *time.Time `json:"received_at,omitempty"`
CheckCount int `json:"check_count"`
WarningCount int `json:"warning_count"`
CriticalCount int `json:"critical_count"`
GateSatisfied bool `json:"gate_satisfied"`
}
type AcceptanceBlockingAlert struct {
ID string `json:"id"`
RuleCode string `json:"rule_code"`
@@ -88,24 +101,25 @@ type AcceptanceGate struct {
}
type AcceptanceSnapshot struct {
ID string `json:"id"`
GeneratedAt time.Time `json:"generated_at"`
TenantID string `json:"tenant_id"`
CandidateVersion string `json:"candidate_version"`
BuildDigest string `json:"build_digest"`
Environment string `json:"environment"`
RunType string `json:"run_type"`
Scope string `json:"scope"`
RedactionPolicy string `json:"redaction_policy"`
RuntimeBuild buildinfo.Info `json:"runtime_build"`
Database AcceptanceDatabaseState `json:"database"`
IntegrationIssues IntegrationIssueSummary `json:"integration_issues"`
ExternalDependencies []AcceptanceDependencyState `json:"external_dependencies"`
Outbox AcceptanceOutboxState `json:"outbox"`
Alerts AcceptanceAlertState `json:"alerts"`
Gates []AcceptanceGate `json:"gates"`
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
Disclaimer string `json:"disclaimer"`
ID string `json:"id"`
GeneratedAt time.Time `json:"generated_at"`
TenantID string `json:"tenant_id"`
CandidateVersion string `json:"candidate_version"`
BuildDigest string `json:"build_digest"`
Environment string `json:"environment"`
RunType string `json:"run_type"`
Scope string `json:"scope"`
RedactionPolicy string `json:"redaction_policy"`
RuntimeBuild buildinfo.Info `json:"runtime_build"`
Database AcceptanceDatabaseState `json:"database"`
IntegrationIssues IntegrationIssueSummary `json:"integration_issues"`
ExternalDependencies []AcceptanceDependencyState `json:"external_dependencies"`
Outbox AcceptanceOutboxState `json:"outbox"`
Alerts AcceptanceAlertState `json:"alerts"`
PlatformHealth AcceptancePlatformHealthState `json:"platform_health"`
Gates []AcceptanceGate `json:"gates"`
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
Disclaimer string `json:"disclaimer"`
}
func (s *Service) ExportAcceptanceSnapshot(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID string, input AcceptanceSnapshotInput) (AcceptanceSnapshot, bool, error) {
@@ -275,6 +289,11 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
); err != nil {
return AcceptanceSnapshot{}, err
}
platformHealth, err := loadAcceptancePlatformHealth(ctx, tx, now)
if err != nil {
return AcceptanceSnapshot{}, err
}
snapshot.PlatformHealth = platformHealth
runtimeSatisfied := input.Environment == "LAB" || (snapshot.RuntimeBuild.Version == input.CandidateVersion && snapshot.RuntimeBuild.Commit != "unknown" && snapshot.RuntimeBuild.BuildTime != "unknown")
dependenciesSatisfied := true
@@ -290,6 +309,7 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
{Code: "integration_issues_clear", Satisfied: snapshot.IntegrationIssues.ReleaseGateSatisfied, Summary: "不得存在未关闭的高或严重级联调问题"},
{Code: "outbox_clear", Satisfied: outboxSatisfied, Summary: "租户及平台级待处理、处理中和死信任务必须清零"},
{Code: "critical_alerts_clear", Satisfied: alertsSatisfied, Summary: "不得存在未解决的高或严重级告警"},
{Code: "platform_health_current", Satisfied: snapshot.PlatformHealth.GateSatisfied, Summary: acceptancePlatformHealthSummary(snapshot.PlatformHealth)},
}
snapshot.OverallGateSatisfied = true
for _, gate := range snapshot.Gates {
@@ -298,6 +318,60 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
return snapshot, nil
}
func loadAcceptancePlatformHealth(ctx context.Context, tx pgx.Tx, now time.Time) (AcceptancePlatformHealthState, error) {
state := AcceptancePlatformHealthState{State: "MISSING"}
var observedAt, receivedAt time.Time
var checksJSON []byte
err := tx.QueryRow(ctx, `
SELECT status,observed_at,received_at,checks
FROM platform_health_snapshot ORDER BY observed_at DESC,id DESC LIMIT 1`).Scan(
&state.Status, &observedAt, &receivedAt, &checksJSON,
)
if errors.Is(err, pgx.ErrNoRows) {
return state, nil
}
if err != nil {
return AcceptancePlatformHealthState{}, err
}
var checks []PlatformHealthCheck
if err = json.Unmarshal(checksJSON, &checks); err != nil {
return AcceptancePlatformHealthState{}, err
}
return evaluateAcceptancePlatformHealth(state.Status, observedAt, receivedAt, checks, now), nil
}
func evaluateAcceptancePlatformHealth(status string, observedAt, receivedAt time.Time, checks []PlatformHealthCheck, now time.Time) AcceptancePlatformHealthState {
state := AcceptancePlatformHealthState{
State: "CURRENT", Status: status, ObservedAt: &observedAt, ReceivedAt: &receivedAt, CheckCount: len(checks),
}
for _, check := range checks {
switch check.Status {
case "warning":
state.WarningCount++
case "critical":
state.CriticalCount++
}
}
if observedAt.Before(now.Add(-platformHealthMaxAge)) || observedAt.After(now.Add(platformHealthFuture)) {
state.State = "STALE"
}
state.GateSatisfied = state.State == "CURRENT" && state.Status == "healthy" && state.WarningCount == 0 && state.CriticalCount == 0
return state
}
func acceptancePlatformHealthSummary(state AcceptancePlatformHealthState) string {
switch state.State {
case "MISSING":
return "尚未收到云端主机健康快照"
case "STALE":
return "云端主机健康快照必须在15分钟内更新"
}
if state.Status != "healthy" || state.WarningCount > 0 || state.CriticalCount > 0 {
return fmt.Sprintf("云端主机存在%d项预警和%d项严重异常", state.WarningCount, state.CriticalCount)
}
return fmt.Sprintf("云端主机健康快照当前有效,共%d项检查", state.CheckCount)
}
func acceptanceRuntimeSummary(environment string) string {
if environment == "LAB" {
return "实验室环境允许开发构建,但候选版本与镜像摘要必须明确"
@@ -59,7 +59,7 @@ func TestAcceptanceSnapshotIsAuditedIdempotentAndTenantScoped(t *testing.T) {
if err != nil || !created {
t.Fatalf("first=%+v created=%t err=%v", first, created, err)
}
if first.ID == "" || first.Database.MigrationVersion < 36 || first.Database.Dirty || first.RedactionPolicy != "ops-acceptance-minimal-v1" || len(first.ExternalDependencies) != 1 || len(first.Gates) != 6 {
if first.ID == "" || first.Database.MigrationVersion < 39 || first.Database.Dirty || first.RedactionPolicy != "ops-acceptance-minimal-v1" || len(first.ExternalDependencies) != 1 || len(first.Gates) != 7 {
t.Fatalf("unexpected snapshot=%+v", first)
}
if first.ExternalDependencies[0].Dependency != "oidc_jwks" || first.ExternalDependencies[0].ServiceName != "api" {
@@ -74,6 +74,9 @@ func TestAcceptanceSnapshotIsAuditedIdempotentAndTenantScoped(t *testing.T) {
if !foundBlockingAlert || first.Alerts.Blocking == 0 || first.Gates[5].Code != "critical_alerts_clear" || first.Gates[5].Satisfied {
t.Fatalf("blocking alert evidence=%+v gates=%+v", first.Alerts, first.Gates)
}
if first.Gates[6].Code != "platform_health_current" || first.PlatformHealth.State == "" {
t.Fatalf("platform health=%+v gates=%+v", first.PlatformHealth, first.Gates)
}
replayed, createdAgain, err := service.ExportAcceptanceSnapshot(ctx, admin, tenantPublicID, key, requestID, input)
if err != nil || createdAgain || replayed.ID != first.ID || !replayed.GeneratedAt.Equal(first.GeneratedAt) {
t.Fatalf("replayed=%+v created=%t err=%v", replayed, createdAgain, err)
@@ -3,6 +3,7 @@ package diagnostics
import (
"strings"
"testing"
"time"
)
func TestValidAcceptanceSnapshotInputLimitsAuditReason(t *testing.T) {
@@ -24,6 +25,52 @@ func TestValidAcceptanceSnapshotInputLimitsAuditReason(t *testing.T) {
}
}
func TestEvaluateAcceptancePlatformHealthBlocksStaleAndUnhealthyReports(t *testing.T) {
now := time.Date(2026, 8, 26, 14, 0, 0, 0, time.UTC)
tests := []struct {
name string
status string
observedAt time.Time
checks []PlatformHealthCheck
state string
satisfied bool
}{
{
name: "current healthy", status: "healthy", observedAt: now.Add(-5 * time.Minute),
checks: []PlatformHealthCheck{{Name: "public_https", Status: "healthy", Value: "http_200"}}, state: "CURRENT", satisfied: true,
},
{
name: "stale healthy", status: "healthy", observedAt: now.Add(-16 * time.Minute),
checks: []PlatformHealthCheck{{Name: "public_https", Status: "healthy", Value: "http_200"}}, state: "STALE", satisfied: false,
},
{
name: "current warning", status: "warning", observedAt: now.Add(-5 * time.Minute),
checks: []PlatformHealthCheck{{Name: "backup", Status: "warning", Value: "late"}}, state: "CURRENT", satisfied: false,
},
{
name: "current critical", status: "critical", observedAt: now.Add(-5 * time.Minute),
checks: []PlatformHealthCheck{{Name: "disk", Status: "critical", Value: "full"}}, state: "CURRENT", satisfied: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
state := evaluateAcceptancePlatformHealth(test.status, test.observedAt, now, test.checks, now)
if state.State != test.state || state.GateSatisfied != test.satisfied {
t.Fatalf("state=%+v", state)
}
})
}
}
func TestAcceptancePlatformHealthSummaryCoversMissingAndCounts(t *testing.T) {
if summary := acceptancePlatformHealthSummary(AcceptancePlatformHealthState{State: "MISSING"}); !strings.Contains(summary, "尚未收到") {
t.Fatalf("missing summary=%q", summary)
}
if summary := acceptancePlatformHealthSummary(AcceptancePlatformHealthState{State: "CURRENT", Status: "warning", WarningCount: 2, CriticalCount: 1}); !strings.Contains(summary, "2项预警") || !strings.Contains(summary, "1项严重异常") {
t.Fatalf("unhealthy summary=%q", summary)
}
}
func TestAcceptanceSnapshotAllowsAllProductionExternalDependencies(t *testing.T) {
input := AcceptanceSnapshotInput{
CandidateVersion: "0.64.0-rc.1", BuildDigest: "sha256:" + strings.Repeat("b", 64),