fix(release): add fast guarded recovery path

This commit is contained in:
修雨
2026-07-17 16:13:15 +08:00
parent ec9ae33a43
commit fa558372d2
16 changed files with 1826 additions and 251 deletions
+193 -3
View File
@@ -598,6 +598,196 @@ func TestPostGoreleaserSkillsZipLayout(t *testing.T) {
}
}
func readReleaseWorkflow(t *testing.T) string {
t.Helper()
workflowPath, err := filepath.Abs(filepath.Join("..", "..", ".github", "workflows", "release.yml"))
if err != nil {
t.Fatalf("Abs(release.yml) error = %v", err)
}
data, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", workflowPath, err)
}
return string(data)
}
func releaseWorkflowSection(t *testing.T, workflow, startMarker, endMarker string) string {
t.Helper()
start := strings.Index(workflow, startMarker)
if start == -1 {
t.Fatalf("release workflow is missing section marker %q", startMarker)
}
end := strings.Index(workflow[start+len(startMarker):], endMarker)
if end == -1 {
t.Fatalf("release workflow section %q is missing end marker %q", startMarker, endMarker)
}
return workflow[start : start+len(startMarker)+end]
}
func TestReleaseWorkflowUsesDedicatedGovernanceIdentity(t *testing.T) {
t.Parallel()
workflow := readReleaseWorkflow(t)
const (
checksCall = "github.rest.checks.listForRef"
immutableCall = `"GET /repos/{owner}/{repo}/immutable-releases"`
governanceID = `github-token: ${{ secrets.RELEASE_GOVERNANCE_TOKEN }}`
)
if got := strings.Count(workflow, checksCall); got != 2 {
t.Fatalf("release workflow Checks API call count = %d, want one tag check and one preflight check", got)
}
if got := strings.Count(workflow, immutableCall); got != 2 {
t.Fatalf("release workflow immutable governance call count = %d, want one tag check and one preflight check", got)
}
if got := strings.Count(workflow, governanceID); got != 2 {
t.Fatalf("release workflow dedicated governance identity count = %d, want one per immutable check", got)
}
sections := map[string]string{
"preflight": releaseWorkflowSection(t, workflow, " governance-preflight:\n", "\n release-contract:\n"),
"tag": releaseWorkflowSection(t, workflow, " release-contract:\n", "\n release:\n"),
}
for name, section := range sections {
for _, required := range []string{
"checks: read",
checksCall,
immutableCall,
governanceID,
"RELEASE_GOVERNANCE_TOKEN with repository Administration read permission is required",
} {
if !strings.Contains(section, required) {
t.Errorf("%s governance path is missing %q", name, required)
}
}
if strings.Contains(section, "contents: write") {
t.Errorf("%s governance path must not grant contents write permission", name)
}
if strings.Contains(section, `github-token: ${{ secrets.GITHUB_TOKEN }}`) {
t.Errorf("%s immutable governance path must not fall back to GITHUB_TOKEN", name)
}
}
}
func TestReleaseWorkflowGovernancePreflightCannotPublish(t *testing.T) {
t.Parallel()
workflow := readReleaseWorkflow(t)
preflight := releaseWorkflowSection(t, workflow, " governance-preflight:\n", "\n release-contract:\n")
for _, required := range []string{
"governance_preflight_commit:",
"governance_preflight_nonce:",
`format('Release governance preflight {0}', inputs.governance_preflight_nonce)`,
"name: Release governance preflight",
"github.event_name == 'workflow_dispatch'",
"EXPECTED_REPOSITORY: DingTalk-Real-AI/dingtalk-workspace-cli",
`DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}`,
`test "$PREFLIGHT_COMMIT" = "$GITHUB_SHA"`,
"governance preflight cannot be combined with npm repair",
} {
if !strings.Contains(workflow, required) {
t.Errorf("governance preflight contract is missing %q", required)
}
}
for _, forbidden := range []string{
"actions/checkout",
"contents: write",
"goreleaser",
"gh release",
"npm publish",
"sync-to-oss",
"sync-to-gitee",
} {
if strings.Contains(preflight, forbidden) {
t.Errorf("governance preflight must not contain publishing behavior %q", forbidden)
}
}
mirror := releaseWorkflowSection(t, workflow, " mirror-gitee-release:\n", "\n repair-npm:\n")
if !strings.Contains(mirror, "needs: [release-contract, release, publish-channels]") {
t.Error("Gitee publication must remain downstream of the verified release target and channel jobs")
}
repair := workflow[strings.Index(workflow, " repair-npm:\n"):]
for _, required := range []string{
"needs: dispatch-contract",
"needs.dispatch-contract.outputs.mode == 'repair_npm'",
} {
if !strings.Contains(repair, required) {
t.Errorf("npm repair dispatch contract is missing %q", required)
}
}
}
func TestReleaseWorkflowRecoveryReusesGuardedJobs(t *testing.T) {
t.Parallel()
workflow := readReleaseWorkflow(t)
for _, required := range []string{
"recover_release_version:",
"recover_release_tag_object:",
"recover_release_commit:",
"recover_failed_run_id:",
"recover_release_nonce:",
"recover_release_confirmation:",
`format('Release recovery {0} at {1} {2}', inputs.recover_release_version, inputs.recover_release_commit, inputs.recover_release_nonce)`,
"workflow_dispatch must select exactly one release mode",
"release recovery confirmation must equal the exact version",
"recover_release_nonce must be bound to the release commit",
"environment: release-recovery",
"prevent_self_review !== true",
"protected_branches !== true",
"can_admins_bypass !== false",
`run.path !== ".github/workflows/release.yml"`,
`run.event !== "push"`,
`["failure", "cancelled", "timed_out", "startup_failure", "stale"].includes(run.conclusion)`,
`run.head_branch !== version`,
`run.head_sha !== commit`,
`tagObject !== expectedTagObject`,
`["ahead", "identical"].includes(comparison.data.status)`,
"already has a public release; use a channel repair instead",
"Bind recovery publication to this workflow run",
"dws-release-recovery run=%s tag-object=%s commit=%s",
"Public release is not bound to this exact recovery run.",
"Public recovery asset differs from this run's sealed artifact",
`ref: process.env.RELEASE_COMMIT`,
`path: tmp/trusted-release-tooling`,
`ref: ${{ github.sha }}`,
`step.name === "Require immutable published GitHub Release"`,
"Require a clean sealed source before GoReleaser",
`git status --porcelain --untracked-files=all`,
} {
if !strings.Contains(workflow, required) {
t.Errorf("release recovery contract is missing %q", required)
}
}
sections := map[string]string{
"release": releaseWorkflowSection(t, workflow, " release:\n", "\n verify-darwin-signatures:\n"),
"publish-release": releaseWorkflowSection(t, workflow, " publish-release:\n", "\n publish-channels:\n"),
"publish-channels": releaseWorkflowSection(t, workflow, " publish-channels:\n", "\n mirror-gitee-release:\n"),
}
for name, section := range sections {
for _, required := range []string{
"needs.release-contract.outputs.release_version",
"ref: ${{ needs.release-contract.outputs.release_commit }}",
"persist-credentials: false",
`tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh`,
} {
if !strings.Contains(section, required) {
t.Errorf("%s does not consume the verified recovery target %q", name, required)
}
}
if strings.Contains(section, "github.event_name == 'workflow_dispatch'") {
t.Errorf("%s must not fork into a recovery-specific publisher", name)
}
}
if strings.Count(workflow, "name: Build signed release artifacts") != 1 ||
strings.Count(workflow, "name: Verify Apple Developer ID signatures") != 1 ||
strings.Count(workflow, "name: Publish immutable GitHub Release") != 1 ||
strings.Count(workflow, "name: Publish npm and mirrors") != 1 {
t.Fatal("normal and recovery publication must share one build/sign/publish job graph")
}
}
func TestReleaseWorkflowUploadsPostProcessedDarwinAssets(t *testing.T) {
t.Parallel()
@@ -762,7 +952,7 @@ func TestReleaseWorkflowUsesAppleCodesignBeforePublication(t *testing.T) {
}
codesign := strings.Index(workflow[verifyJob:publishJob], "codesign --verify --strict --verbose=4")
publish := strings.Index(workflow[publishJob:], `gh release edit "$GITHUB_REF_NAME" --draft=false`)
publish := strings.Index(workflow[publishJob:], `gh release edit "$RELEASE_VERSION" --draft=false`)
if codesign == -1 || publish == -1 {
t.Fatal("macOS codesign verification and explicit Draft publication are required")
}
@@ -848,7 +1038,7 @@ func TestReleaseWorkflowOpensHomebrewPROnlyForOfficialStableTags(t *testing.T) {
"./scripts/release/publish-homebrew-formula.sh",
"secrets.HOMEBREW_PR_TOKEN",
"DWS_TAP_PR_REPOSITORY",
"automation/homebrew-${{ github.ref_name }}",
"automation/homebrew-${{ needs.release-contract.outputs.release_version }}",
} {
if !strings.Contains(section, required) {
t.Errorf("Homebrew publication step is missing %q", required)
@@ -891,7 +1081,7 @@ func TestReleaseWorkflowOpensVersionedHomebrewPRForBetaTags(t *testing.T) {
"dist/homebrew/dingtalk-workspace-cli-beta.rb",
"Formula/dingtalk-workspace-cli-beta.rb",
"secrets.HOMEBREW_PR_TOKEN",
"automation/homebrew-beta-${{ github.ref_name }}",
"automation/homebrew-beta-${{ needs.release-contract.outputs.release_version }}",
} {
if !strings.Contains(section, required) {
t.Errorf("beta Homebrew PR step is missing %q", required)