Files
dingtalk-workspace-cli/test/scripts/release_script_test.go
T

1323 lines
51 KiB
Go

package scripts_test
import (
"crypto/sha256"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"testing"
)
var releasePlatformAssets = []string{
"dws-darwin-amd64.tar.gz",
"dws-darwin-arm64.tar.gz",
"dws-linux-amd64.tar.gz",
"dws-linux-arm64.tar.gz",
"dws-windows-amd64.zip",
"dws-windows-arm64.zip",
}
func writeVersionedReleaseArchive(t *testing.T, dist, asset, version string) {
t.Helper()
stage := t.TempDir()
binary := "dws"
if strings.HasSuffix(asset, ".zip") {
binary = "dws.exe"
}
mustWriteFile(t, filepath.Join(stage, binary), []byte("fake release binary\n"+version+"\n"), 0o755)
if strings.HasSuffix(asset, ".zip") {
mustRun(t, stage, "zip", "-q", filepath.Join(dist, asset), binary)
return
}
mustRun(t, stage, "tar", "-czf", filepath.Join(dist, asset), binary)
}
func writeReleaseChecksums(t *testing.T, dist string, includeSkills bool) {
t.Helper()
assets := append([]string{}, releasePlatformAssets...)
if includeSkills {
assets = append(assets, "dws-skills.zip")
}
sort.Strings(assets)
var lines []string
for _, asset := range assets {
data, err := os.ReadFile(filepath.Join(dist, asset))
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", asset, err)
}
lines = append(lines, fmt.Sprintf("%x %s", sha256.Sum256(data), asset))
}
mustWriteFile(t, filepath.Join(dist, "checksums.txt"), []byte(strings.Join(lines, "\n")+"\n"), 0o644)
}
func seedVersionedReleaseArtifacts(t *testing.T, dist, version string) {
t.Helper()
if err := os.MkdirAll(dist, 0o755); err != nil {
t.Fatalf("MkdirAll(%s) error = %v", dist, err)
}
for _, asset := range releasePlatformAssets {
writeVersionedReleaseArchive(t, dist, asset, version)
}
mustWriteFile(t, filepath.Join(dist, "dws-skills.zip"), []byte("fake skills\n"), 0o644)
writeReleaseChecksums(t, dist, true)
}
type releaseTestRepo struct {
root string
remote string
contract string
prepare string
releaseCmd string
lib string
verify string
}
func newReleaseTestRepo(t *testing.T) *releaseTestRepo {
t.Helper()
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
base := t.TempDir()
root := filepath.Join(base, "work")
remote := filepath.Join(base, "remote.git")
mustRun(t, base, "git", "init", "--bare", remote)
mustRun(t, base, "git", "init", "-b", "main", root)
mustRun(t, root, "git", "config", "user.name", "Release Test")
mustRun(t, root, "git", "config", "user.email", "release-test@example.com")
mustWriteFile(t, filepath.Join(root, "CHANGELOG.md"), []byte(releaseChangelog()), 0o644)
mustWriteFile(t, filepath.Join(root, "seed.txt"), []byte("initial\n"), 0o644)
mustRun(t, root, "git", "add", ".")
mustRun(t, root, "git", "commit", "-m", "initial stable")
mustRun(t, root, "git", "tag", "-a", "v1.0.0", "-m", "Release v1.0.0")
mustRun(t, root, "git", "remote", "add", "origin", remote)
mustRun(t, root, "git", "push", "-u", "origin", "main")
mustRun(t, root, "git", "push", "origin", "v1.0.0")
return &releaseTestRepo{
root: root,
remote: remote,
contract: filepath.Join(sourceRoot, "scripts", "release", "release-contract.sh"),
prepare: filepath.Join(sourceRoot, "scripts", "release", "prepare-changelog.sh"),
releaseCmd: filepath.Join(sourceRoot, "scripts", "release", "release.sh"),
lib: filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh"),
verify: filepath.Join(sourceRoot, "scripts", "release", "verify-release-artifacts.sh"),
}
}
func TestReleaseVersionOrdering(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
lib := filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh")
tests := []struct {
candidate string
baseline string
greater bool
}{
{candidate: "v1.0.2", baseline: "v1.0.1", greater: true},
{candidate: "v1.1.0-beta.1", baseline: "v1.0.9-beta.9", greater: true},
{candidate: "v1.0.1-beta.2", baseline: "v1.0.1-beta.1", greater: true},
{candidate: "v1.0.1", baseline: "v1.0.1-beta.9", greater: true},
{candidate: "v1.0.1-beta.1", baseline: "v1.0.1-beta.2", greater: false},
{candidate: "v1.0.1-beta.1", baseline: "v1.0.1", greater: false},
{candidate: "v1.0.1", baseline: "v1.0.1", greater: false},
}
for _, test := range tests {
cmd := exec.Command("sh", "-c", `. "$1"; release_version_is_greater "$2" "$3"`, "sh", lib, test.candidate, test.baseline)
err := cmd.Run()
if (err == nil) != test.greater {
t.Fatalf("release_version_is_greater(%s, %s) error = %v, want greater=%v", test.candidate, test.baseline, err, test.greater)
}
}
}
func TestReleaseLibHelpersPreserveCallerVariables(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
lib := filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh")
changelogPath := filepath.Join(t.TempDir(), "CHANGELOG.md")
mustWriteFile(t, changelogPath, []byte("## [1.2.3] - 2026-07-16\n\n- Test release.\n"), 0o644)
script := `
. "$1"
version=caller-version
expected=caller-expected
actual=caller-actual
candidate=caller-candidate
baseline=caller-baseline
changelog=caller-changelog
semver=caller-semver
output=caller-output
tmp=caller-tmp
status=caller-status
release_channel_for_version v1.2.3 >/dev/null
release_validate_version_channel stable v1.2.3
release_core_tag v1.2.3-beta.1 >/dev/null
release_core_is_greater v1.2.4 v1.2.3
release_extract_changelog "$2" 1.2.3 - >/dev/null
printf '%s\n' "$version" "$expected" "$actual" "$candidate" "$baseline" \
"$changelog" "$semver" "$output" "$tmp" "$status"
`
output, err := exec.Command("sh", "-c", script, "sh", lib, changelogPath).CombinedOutput()
if err != nil {
t.Fatalf("release helper variable isolation error = %v\noutput:\n%s", err, output)
}
want := strings.Join([]string{
"caller-version",
"caller-expected",
"caller-actual",
"caller-candidate",
"caller-baseline",
"caller-changelog",
"caller-semver",
"caller-output",
"caller-tmp",
"caller-status",
}, "\n") + "\n"
if string(output) != want {
t.Fatalf("release helpers changed caller variables:\ngot:\n%s\nwant:\n%s", output, want)
}
}
func TestReleaseNpmPackingIgnoresLifecycleScripts(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
packageDir := filepath.Join(t.TempDir(), "package")
marker := filepath.Join(t.TempDir(), "prepack-ran")
manifest := fmt.Sprintf(`{
"name": "dingtalk-workspace-cli",
"version": "1.2.3",
"scripts": {"prepack": "touch %s"},
"files": ["README.md"]
}
`, marker)
mustWriteFile(t, filepath.Join(packageDir, "package.json"), []byte(manifest), 0o644)
mustWriteFile(t, filepath.Join(packageDir, "README.md"), []byte("test package\n"), 0o644)
outputTarball := filepath.Join(t.TempDir(), "package.tgz")
cmd := exec.Command("sh", filepath.Join(sourceRoot, "scripts", "release", "pack-npm-package.sh"), packageDir, outputTarball)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("pack-npm-package error = %v\noutput:\n%s", err, output)
}
if !strings.HasPrefix(strings.TrimSpace(string(output)), "sha512-") {
t.Fatalf("pack output is not an integrity value: %s", output)
}
if _, err := os.Stat(outputTarball); err != nil {
t.Fatalf("packed tarball missing: %v", err)
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("npm prepack lifecycle unexpectedly ran: %v", err)
}
}
func TestReleaseNpmStagingRejectsUnexpectedLifecycleScripts(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
root := t.TempDir()
source := filepath.Join(root, "source")
dist := filepath.Join(root, "dist")
mustWriteFile(t, filepath.Join(source, "build", "npm", "install.js"), []byte("\n"), 0o644)
mustWriteFile(t, filepath.Join(source, "build", "npm", "bin", "dws.js"), []byte("\n"), 0o755)
mustWriteFile(t, filepath.Join(source, "build", "npm", "README.md"), []byte("test\n"), 0o644)
mustWriteFile(t, filepath.Join(source, "build", "npm", "package.json.tmpl"), []byte(`{
"name": "dingtalk-workspace-cli",
"version": "__VERSION__",
"bin": {"dws": "./bin/dws.js"},
"scripts": {"postinstall": "node install.js", "prepublishOnly": "node steal.js"}
}
`), 0o644)
cmd := exec.Command("sh", filepath.Join(sourceRoot, "scripts", "release", "stage-npm-package.sh"), "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_SOURCE_ROOT="+source, "DWS_PACKAGE_DIST_DIR="+dist)
output, err := cmd.CombinedOutput()
if err == nil || !strings.Contains(string(output), "unexpected npm lifecycle scripts") {
t.Fatalf("unexpected lifecycle script was not rejected: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseGitHubAssetSetIsExact(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
binDir := t.TempDir()
fakeGH := filepath.Join(binDir, "gh")
mustWriteFile(t, fakeGH, []byte("#!/bin/sh\nset -eu\nprintf '%s\\n' \"$GH_ASSETS\"\n"), 0o755)
exact := strings.Join([]string{
"dws-windows-arm64.zip",
"dws-linux-amd64.tar.gz",
"checksums.txt",
"dws-skills.zip",
"dws-darwin-amd64.tar.gz",
"dws-windows-amd64.zip",
"dws-linux-arm64.tar.gz",
"dws-darwin-arm64.tar.gz",
}, "\n")
script := filepath.Join(sourceRoot, "scripts", "release", "verify-github-release-assets.sh")
run := func(assets string) (string, error) {
cmd := exec.Command("sh", script, "v1.2.3")
cmd.Env = []string{
"PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
"HOME=" + t.TempDir(),
"GITHUB_REPOSITORY=owner/repo",
"GH_ASSETS=" + assets,
}
output, err := cmd.CombinedOutput()
return string(output), err
}
if output, err := run(exact); err != nil {
t.Fatalf("exact GitHub assets rejected: %v\n%s", err, output)
}
if output, err := run(exact + "\nmalware.exe"); err == nil || !strings.Contains(output, "exactly the supported assets") {
t.Fatalf("extra GitHub asset was not rejected: err=%v\n%s", err, output)
}
}
func TestReleaseDeliveredStableRequiresSuccessfulPublicDelivery(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
r := newReleaseTestRepo(t)
commit := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "v1.0.0^{commit}"))
binDir := t.TempDir()
fakeCurl := filepath.Join(binDir, "curl")
mustWriteFile(t, fakeCurl, []byte(`#!/bin/sh
set -eu
for arg in "$@"; do last="$arg"; done
case "$last" in
*/releases/tags/*)
printf '{"tag_name":"v1.0.0","draft":false,"prerelease":false}\n'
;;
*/actions/workflows/release.yml/runs*)
printf '{"workflow_runs":[{"head_sha":"%s","head_branch":"v1.0.0","conclusion":"%s"}]}\n' "$EXPECTED_COMMIT" "$RUN_CONCLUSION"
;;
*) exit 1 ;;
esac
`), 0o755)
script := filepath.Join(sourceRoot, "scripts", "release", "verify-delivered-stable.sh")
run := func(conclusion string) (string, error) {
cmd := exec.Command("sh", script, "v1.0.0", commit)
cmd.Dir = r.root
cmd.Env = []string{
"PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
"HOME=" + t.TempDir(),
"DWS_RELEASE_OFFICIAL_REPOSITORY=owner/repo",
"EXPECTED_COMMIT=" + commit,
"RUN_CONCLUSION=" + conclusion,
}
output, err := cmd.CombinedOutput()
return string(output), err
}
if output, err := run("success"); err != nil {
t.Fatalf("delivered stable was rejected: %v\n%s", err, output)
}
if output, err := run("failure"); err == nil || !strings.Contains(output, "did not complete successfully") {
t.Fatalf("orphan stable tag was not rejected: err=%v\n%s", err, output)
}
}
func TestReleaseDeliveredStableAcceptsOnlyPinnedSuccessfulRecovery(t *testing.T) {
const (
tag = "v1.0.52"
commit = "4e59f9aa7ab057da8d5512ae9818fb66d4c6a045"
runID = "29380892754"
workflowSHA = "434251695c19ebbfe2a2240d2eddce2d56af07b7"
)
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
root := t.TempDir()
mustRun(t, root, "git", "init")
binDir := t.TempDir()
fakeCurl := filepath.Join(binDir, "curl")
mustWriteFile(t, fakeCurl, []byte(`#!/bin/sh
set -eu
for arg in "$@"; do last="$arg"; done
case "$last" in
*/releases/tags/*)
printf '{"tag_name":"v1.0.52","draft":false,"prerelease":false}\n'
;;
*/actions/workflows/release.yml/runs*)
printf '{"workflow_runs":[]}\n'
;;
*/actions/runs/29380892754/jobs*)
printf '{"jobs":[{"name":"release","status":"completed","conclusion":"success","head_sha":"%s"},{"name":"verify-darwin-signatures","status":"completed","conclusion":"success","head_sha":"%s"},{"name":"publish-release","status":"completed","conclusion":"%s","head_sha":"%s"}]}\n' "$WORKFLOW_SHA" "$WORKFLOW_SHA" "$PUBLISH_CONCLUSION" "$WORKFLOW_SHA"
;;
*/actions/runs/29380892754)
printf '{"event":"workflow_dispatch","status":"completed","conclusion":"success","head_sha":"%s","head_branch":"codex/recover-v1.0.52","run_attempt":1,"path":".github/workflows/release.yml","repository":{"full_name":"owner/repo"}}\n' "$RUN_HEAD_SHA"
;;
*) exit 1 ;;
esac
`), 0o755)
script := filepath.Join(sourceRoot, "scripts", "release", "verify-delivered-stable.sh")
run := func(runHeadSHA, publishConclusion string) (string, error) {
cmd := exec.Command("sh", script, tag, commit)
cmd.Dir = root
cmd.Env = []string{
"PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
"HOME=" + t.TempDir(),
"DWS_RELEASE_OFFICIAL_REPOSITORY=owner/repo",
"WORKFLOW_SHA=" + workflowSHA,
"RUN_HEAD_SHA=" + runHeadSHA,
"PUBLISH_CONCLUSION=" + publishConclusion,
}
output, err := cmd.CombinedOutput()
return string(output), err
}
if output, err := run(workflowSHA, "success"); err != nil || !strings.Contains(output, "reviewed recovery run "+runID) {
t.Fatalf("pinned stable recovery was rejected: err=%v\noutput:\n%s", err, output)
}
if output, err := run(strings.Repeat("0", 40), "success"); err == nil || !strings.Contains(output, "does not match the pinned delivery proof") {
t.Fatalf("mismatched recovery workflow passed: err=%v\noutput:\n%s", err, output)
}
if output, err := run(workflowSHA, "failure"); err == nil || !strings.Contains(output, "missing successful job publish-release") {
t.Fatalf("failed recovery delivery job passed: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseWorkflowDeliveryAcceptsOnlySharedProtectedRecovery(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
binDir := t.TempDir()
fakeCurl := filepath.Join(binDir, "curl")
mustWriteFile(t, fakeCurl, []byte(`#!/bin/sh
set -eu
for argument in "$@"; do endpoint="$argument"; done
case "$endpoint" in
*event=push*)
printf '{"workflow_runs":[]}\n'
;;
*event=workflow_dispatch*)
printf '{"workflow_runs":[{"id":42,"display_title":"Release recovery %s at %s %s-1700000000-42","event":"workflow_dispatch","status":"completed","conclusion":"success","head_branch":"main","head_sha":"%s","path":".github/workflows/release.yml","repository":{"full_name":"owner/repo"}}]}\n' "$TAG" "$RELEASE_COMMIT" "$RELEASE_COMMIT" "$WORKFLOW_SHA"
;;
*/compare/*...main)
printf '{"status":"ahead"}\n'
;;
*/actions/runs/42/jobs*)
printf '{"jobs":['
printf '{"name":"Build signed release artifacts","status":"completed","conclusion":"success","head_sha":"%s"},' "$WORKFLOW_SHA"
printf '{"name":"Verify Apple Developer ID signatures","status":"completed","conclusion":"success","head_sha":"%s"},' "$WORKFLOW_SHA"
printf '{"name":"Publish immutable GitHub Release","status":"completed","conclusion":"success","head_sha":"%s"}' "$WORKFLOW_SHA"
if [ "${MISSING_CHANNELS:-0}" != 1 ]; then
printf ',{"name":"Publish npm and mirrors","status":"completed","conclusion":"success","head_sha":"%s"}' "$WORKFLOW_SHA"
fi
printf ']}\n'
;;
*) exit 1 ;;
esac
`), 0o755)
script := filepath.Join(sourceRoot, "scripts", "release", "verify-release-workflow-delivery.sh")
tag := "v1.2.3-beta.1"
commit := strings.Repeat("a", 40)
workflowSHA := strings.Repeat("b", 40)
run := func(missingChannels bool) (string, error) {
cmd := exec.Command("sh", script, tag, commit)
missing := "0"
if missingChannels {
missing = "1"
}
cmd.Env = []string{
"PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
"HOME=" + t.TempDir(),
"DWS_RELEASE_OFFICIAL_REPOSITORY=owner/repo",
"TAG=" + tag,
"RELEASE_COMMIT=" + commit,
"WORKFLOW_SHA=" + workflowSHA,
"MISSING_CHANNELS=" + missing,
}
output, err := cmd.CombinedOutput()
return string(output), err
}
output, err := run(false)
if err != nil || !strings.Contains(output, "protected recovery run 42") {
t.Fatalf("protected shared recovery was rejected: err=%v\noutput:\n%s", err, output)
}
output, err = run(true)
if err == nil || !strings.Contains(output, "did not complete the shared release job graph") {
t.Fatalf("incomplete recovery job graph passed: err=%v\noutput:\n%s", err, output)
}
}
func releaseChangelog(sections ...string) string {
text := "# Changelog\n\n## [Unreleased]\n\n"
for _, section := range sections {
text += section
if !strings.HasSuffix(text, "\n\n") {
text += "\n"
}
}
text += "## [1.0.0] - 2026-07-01\n\n### Changed\n\n- Initial release.\n"
return text
}
func betaSection() string {
return "## [1.0.1-beta.1] - 2026-07-11\n\n### Changed\n\n- Validate the sealed beta candidate.\n\n"
}
func stableSection() string {
return "## [1.0.1] - 2026-07-11\n\nThis release promotes the sealed `v1.0.1-beta.1` contents to stable.\n\n### Changed\n\n- Publish the validated candidate to the stable channel.\n\n"
}
func (r *releaseTestRepo) commitAndPush(t *testing.T, message string) {
t.Helper()
mustRun(t, r.root, "git", "add", ".")
mustRun(t, r.root, "git", "commit", "-m", message)
mustRun(t, r.root, "git", "push", "origin", "main")
}
func (r *releaseTestRepo) seedBeta(t *testing.T) {
t.Helper()
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
mustWriteFile(t, filepath.Join(r.root, "feature.txt"), []byte("sealed candidate\n"), 0o644)
r.commitAndPush(t, "prepare beta")
mustRun(t, r.root, "git", "tag", "-a", "v1.0.1-beta.1", "-m", "Release v1.0.1-beta.1", "-m", "Channel: prerelease")
mustRun(t, r.root, "git", "push", "origin", "v1.0.1-beta.1")
}
func runReleaseScript(t *testing.T, workdir, script string, args ...string) (string, error) {
t.Helper()
cmd := exec.Command("sh", append([]string{script}, args...)...)
cmd.Dir = workdir
cmd.Env = append(os.Environ(), "DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE=1")
if remote, err := exec.Command("git", "-C", workdir, "remote", "get-url", "origin").Output(); err == nil {
cmd.Env = append(cmd.Env, "DWS_RELEASE_OFFICIAL_TAGS_URL="+strings.TrimSpace(string(remote)))
}
output, err := cmd.CombinedOutput()
return string(output), err
}
func TestReleaseContractAcceptsPrereleaseAndWritesNotes(t *testing.T) {
r := newReleaseTestRepo(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "prepare beta changelog")
notes := filepath.Join(t.TempDir(), "notes.md")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--context", "local",
"--remote", "origin",
"--notes-output", notes,
)
if err != nil {
t.Fatalf("release contract error = %v\noutput:\n%s", err, output)
}
if !strings.Contains(output, "Release contract passed") {
t.Fatalf("contract output missing success:\n%s", output)
}
notesData, err := os.ReadFile(notes)
if err != nil {
t.Fatalf("ReadFile(notes) error = %v", err)
}
if !strings.Contains(string(notesData), "Validate the sealed beta candidate") {
t.Fatalf("notes did not come from exact changelog section:\n%s", notesData)
}
}
func TestReleaseContractRejectsInvalidVersionChannelPairs(t *testing.T) {
r := newReleaseTestRepo(t)
tests := []struct {
channel string
version string
}{
{channel: "prerelease", version: "v1.0.1"},
{channel: "stable", version: "v1.0.1-beta.1"},
{channel: "prerelease", version: "v1.0.1-rc.1"},
{channel: "prerelease", version: "v1.0.1-preview"},
{channel: "stable", version: "1.0.1"},
{channel: "stable", version: "v01.0.1"},
{channel: "prerelease", version: "v1.0.1-beta.0"},
{channel: "prerelease", version: "v1.0.1-beta.2"},
}
for _, test := range tests {
t.Run(test.channel+"_"+strings.ReplaceAll(test.version, ".", "_"), func(t *testing.T) {
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", test.channel,
"--version", test.version,
)
if err == nil {
t.Fatalf("invalid pair unexpectedly passed:\n%s", output)
}
})
}
}
func TestReleaseContractRejectsBadChangelogSections(t *testing.T) {
tests := []struct {
name string
sections string
want string
}{
{name: "missing", sections: "", want: "exactly one section"},
{name: "empty", sections: "## [1.0.1-beta.1] - 2026-07-11\n\n", want: "must contain release notes"},
{name: "placeholder", sections: "## [1.0.1-beta.1] - 2026-07-11\n\n### Changed\n\n- TODO: write this.\n\n", want: "TODO/TBD"},
{name: "duplicate", sections: betaSection() + betaSection(), want: "exactly one section"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := newReleaseTestRepo(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(test.sections)), 0o644)
mustWriteFile(t, filepath.Join(r.root, "candidate.txt"), []byte(test.name+"\n"), 0o644)
r.commitAndPush(t, "write candidate changelog")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--remote", "origin",
)
if err == nil {
t.Fatalf("bad changelog unexpectedly passed:\n%s", output)
}
if !strings.Contains(output, test.want) {
t.Fatalf("output missing %q:\n%s", test.want, output)
}
})
}
}
func TestReleaseContractRejectsDirtyOrUnsyncedMain(t *testing.T) {
r := newReleaseTestRepo(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "prepare beta")
mustWriteFile(t, filepath.Join(r.root, "dirty.txt"), []byte("dirty\n"), 0o644)
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--remote", "origin",
)
if err == nil || !strings.Contains(output, "worktree must be clean") {
t.Fatalf("dirty worktree was not blocked: err=%v\noutput:\n%s", err, output)
}
mustRun(t, r.root, "git", "add", "dirty.txt")
mustRun(t, r.root, "git", "commit", "-m", "local commit not pushed")
output, err = runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--remote", "origin",
)
if err == nil || !strings.Contains(output, "must exactly match origin/main") {
t.Fatalf("unsynced main was not blocked: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseContractStablePromotionAllowsOnlyChangelogDiff(t *testing.T) {
t.Run("sealed", func(t *testing.T) {
r := newReleaseTestRepo(t)
r.seedBeta(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(stableSection(), betaSection())), 0o644)
r.commitAndPush(t, "prepare stable changelog")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "stable",
"--version", "v1.0.1",
"--from-beta", "v1.0.1-beta.1",
"--remote", "origin",
)
if err != nil {
t.Fatalf("sealed stable promotion error = %v\noutput:\n%s", err, output)
}
})
t.Run("source drift", func(t *testing.T) {
r := newReleaseTestRepo(t)
r.seedBeta(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(stableSection(), betaSection())), 0o644)
mustWriteFile(t, filepath.Join(r.root, "drift.txt"), []byte("untested change\n"), 0o644)
r.commitAndPush(t, "drift after beta")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "stable",
"--version", "v1.0.1",
"--from-beta", "v1.0.1-beta.1",
"--remote", "origin",
)
if err == nil {
t.Fatalf("drifted stable promotion unexpectedly passed:\n%s", output)
}
if !strings.Contains(output, "only CHANGELOG.md may differ") || !strings.Contains(output, "drift.txt") {
t.Fatalf("drift output is not actionable:\n%s", output)
}
})
}
func TestReleaseContractCIReadsStableBaselineFromAnnotatedTag(t *testing.T) {
r := newReleaseTestRepo(t)
r.seedBeta(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(stableSection(), betaSection())), 0o644)
r.commitAndPush(t, "prepare stable changelog")
mustRun(t, r.root, "git", "tag", "-a", "v1.0.1", "-m", "Release v1.0.1", "-m", "Channel: stable", "-m", "From-Beta: v1.0.1-beta.1")
mustRun(t, r.root, "git", "push", "origin", "v1.0.1")
mustRun(t, r.root, "git", "checkout", "--detach", "v1.0.1")
metadata := filepath.Join(t.TempDir(), "metadata")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "stable",
"--version", "v1.0.1",
"--context", "ci",
"--remote", "origin",
"--metadata-output", metadata,
)
if err != nil {
t.Fatalf("CI stable contract error = %v\noutput:\n%s", err, output)
}
metadataData, err := os.ReadFile(metadata)
if err != nil {
t.Fatalf("ReadFile(metadata) error = %v", err)
}
if !strings.Contains(string(metadataData), "from_beta=v1.0.1-beta.1") {
t.Fatalf("metadata missing annotated tag baseline:\n%s", metadataData)
}
}
func TestReleaseContractCIRejectsLightweightTag(t *testing.T) {
r := newReleaseTestRepo(t)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "prepare beta")
mustRun(t, r.root, "git", "tag", "v1.0.1-beta.1")
mustRun(t, r.root, "git", "push", "origin", "v1.0.1-beta.1")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--context", "ci",
"--remote", "origin",
)
if err == nil {
t.Fatalf("lightweight release tag unexpectedly passed:\n%s", output)
}
if !strings.Contains(output, "must be annotated") {
t.Fatalf("output missing annotated-tag guidance:\n%s", output)
}
}
func TestReleaseContractCIAllowsMainToAdvanceAfterTagSeal(t *testing.T) {
r := newReleaseTestRepo(t)
r.seedBeta(t)
mustWriteFile(t, filepath.Join(r.root, "after-seal.txt"), []byte("main advanced\n"), 0o644)
r.commitAndPush(t, "advance main after beta seal")
mustRun(t, r.root, "git", "checkout", "--detach", "v1.0.1-beta.1")
output, err := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--context", "ci",
"--remote", "origin",
)
if err != nil {
t.Fatalf("sealed tag should remain valid after main advances: %v\noutput:\n%s", err, output)
}
}
func TestReleasePrepareChangelogCreatesGuardedTemplate(t *testing.T) {
r := newReleaseTestRepo(t)
releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644)
releaseCopyFile(t, r.prepare, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), 0o755)
r.commitAndPush(t, "install changelog preparation")
cmd := exec.Command("sh", filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), "prerelease", "v1.0.1-beta.1")
cmd.Dir = r.root
cmd.Env = append(os.Environ(), "DWS_RELEASE_DATE=2026-07-11")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("prepare changelog error = %v\noutput:\n%s", err, output)
}
changelog, err := os.ReadFile(filepath.Join(r.root, "CHANGELOG.md"))
if err != nil {
t.Fatalf("ReadFile(CHANGELOG.md) error = %v", err)
}
if !strings.Contains(string(changelog), "## [1.0.1-beta.1] - 2026-07-11") || !strings.Contains(string(changelog), "TODO") {
t.Fatalf("prepared changelog missing guarded template:\n%s", changelog)
}
r.commitAndPush(t, "commit unfinished release notes")
contractOutput, contractErr := runReleaseScript(t, r.root, r.contract,
"--repo-root", r.root,
"--channel", "prerelease",
"--version", "v1.0.1-beta.1",
"--remote", "origin",
)
if contractErr == nil || !strings.Contains(contractOutput, "TODO/TBD") {
t.Fatalf("unfinished template was not blocked: err=%v\noutput:\n%s", contractErr, contractOutput)
}
}
func TestReleasePrepareChangelogKeepsUnreleasedContentAboveNewVersion(t *testing.T) {
r := newReleaseTestRepo(t)
releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644)
releaseCopyFile(t, r.prepare, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Changed\n\n- Keep this unreleased note.\n\n## [1.0.0] - 2026-07-01\n\n### Changed\n\n- Initial release.\n"), 0o644)
r.commitAndPush(t, "add unreleased changelog note")
cmd := exec.Command("sh", filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), "prerelease", "v1.0.1-beta.1")
cmd.Dir = r.root
cmd.Env = append(os.Environ(), "DWS_RELEASE_DATE=2026-07-11")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("prepare changelog error = %v\noutput:\n%s", err, output)
}
data, err := os.ReadFile(filepath.Join(r.root, "CHANGELOG.md"))
if err != nil {
t.Fatalf("ReadFile(CHANGELOG.md) error = %v", err)
}
content := string(data)
positions := []int{
strings.Index(content, "## [Unreleased]"),
strings.Index(content, "- Keep this unreleased note."),
strings.Index(content, "## [1.0.1-beta.1] - 2026-07-11"),
strings.Index(content, "## [1.0.0] - 2026-07-01"),
}
for index, position := range positions {
if position < 0 {
t.Fatalf("prepared changelog is missing expected marker %d:\n%s", index, content)
}
}
for index := 1; index < len(positions); index++ {
if positions[index-1] >= positions[index] {
t.Fatalf("prepared changelog order is invalid: %v\n%s", positions, content)
}
}
}
func TestReleaseMirrorUsesChannelSpecificPointer(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
script := filepath.Join(sourceRoot, "scripts", "release", "sync-to-oss.sh")
for _, test := range []struct {
name string
version string
channel string
want string
doNotWant string
installer bool
}{
{name: "prerelease", version: "v1.2.3-beta.1", channel: "prerelease", want: "/beta.txt", doNotWant: "/latest.txt", installer: false},
{name: "stable", version: "v1.2.3", channel: "stable", want: "/latest.txt", doNotWant: "/beta.txt", installer: true},
} {
t.Run(test.name, func(t *testing.T) {
root := t.TempDir()
dist := filepath.Join(root, "dist")
seedVersionedReleaseArtifacts(t, dist, test.version)
logPath := filepath.Join(root, "ossutil.log")
argsLogPath := filepath.Join(root, "ossutil-args.log")
fakeOSSUtil := filepath.Join(root, "ossutil")
mustWriteFile(t, fakeOSSUtil, []byte("#!/bin/sh\nset -eu\nprintf '%s\\n' \"$@\" >> \"$OSSUTIL_ARGS_LOG\"\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) echo 'ErrorCode=NoSuchKey' >&2; exit 1 ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"), 0o755)
cmd := exec.Command("bash", script)
cmd.Dir = sourceRoot
cmd.Env = append(os.Environ(),
"DIST_DIR="+dist,
"VERSION="+test.version,
"DWS_RELEASE_CHANNEL="+test.channel,
"OSS_ACCESS_KEY_ID=test-key",
"OSS_ACCESS_KEY_SECRET=test-secret",
"OSS_ENDPOINT=https://oss.example.com",
"OSS_BUCKET=test-bucket",
"OSS_PREFIX=dws",
"OSSUTIL="+fakeOSSUtil,
"OSSUTIL_LOG="+logPath,
"OSSUTIL_ARGS_LOG="+argsLogPath,
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("sync-to-oss error = %v\noutput:\n%s", err, output)
}
logData, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile(ossutil log) error = %v", err)
}
if !strings.Contains(string(logData), test.want) {
t.Fatalf("mirror log missing %q:\n%s", test.want, logData)
}
if strings.Contains(string(logData), test.doNotWant) {
t.Fatalf("mirror log unexpectedly contains %q:\n%s", test.doNotWant, logData)
}
hasInstaller := strings.Contains(string(logData), "/dws/install.sh")
if hasInstaller != test.installer {
t.Fatalf("installer upload = %v, want %v:\n%s", hasInstaller, test.installer, logData)
}
argsData, err := os.ReadFile(argsLogPath)
if err != nil {
t.Fatalf("ReadFile(ossutil args log) error = %v", err)
}
for _, forbidden := range []string{"test-key", "test-secret", "--access-key-id", "--access-key-secret"} {
if strings.Contains(string(argsData), forbidden) {
t.Fatalf("ossutil argv exposed %q:\n%s", forbidden, argsData)
}
}
})
}
}
func TestReleaseMirrorFailsClosedWhenPointerCannotBeRead(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
script := filepath.Join(sourceRoot, "scripts", "release", "sync-to-oss.sh")
for _, test := range []struct {
name string
readAction string
want string
}{
{name: "transport error", readAction: "echo 'connection timed out' >&2; exit 7", want: "Could not read OSS beta.txt"},
{name: "empty pointer", readAction: ": > \"$last\"; exit 0", want: "read successfully but is empty"},
} {
t.Run(test.name, func(t *testing.T) {
root := t.TempDir()
dist := filepath.Join(root, "dist")
seedVersionedReleaseArtifacts(t, dist, "v1.2.3-beta.1")
logPath := filepath.Join(root, "ossutil.log")
fakeOSSUtil := filepath.Join(root, "ossutil")
scriptText := "#!/bin/sh\nset -eu\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) " + test.readAction + " ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"
mustWriteFile(t, fakeOSSUtil, []byte(scriptText), 0o755)
cmd := exec.Command("bash", script)
cmd.Dir = sourceRoot
cmd.Env = append(os.Environ(),
"DIST_DIR="+dist,
"VERSION=v1.2.3-beta.1",
"DWS_RELEASE_CHANNEL=prerelease",
"OSS_ACCESS_KEY_ID=test-key",
"OSS_ACCESS_KEY_SECRET=test-secret",
"OSS_ENDPOINT=https://oss.example.com",
"OSS_BUCKET=test-bucket",
"OSS_PREFIX=dws",
"OSSUTIL="+fakeOSSUtil,
"OSSUTIL_LOG="+logPath,
)
output, err := cmd.CombinedOutput()
if err == nil || !strings.Contains(string(output), test.want) {
t.Fatalf("pointer failure was not closed: err=%v, want=%q\noutput:\n%s", err, test.want, output)
}
if logData, readErr := os.ReadFile(logPath); readErr == nil && strings.Contains(string(logData), "/beta.txt") {
t.Fatalf("failed pointer read still published beta pointer:\n%s", logData)
}
})
}
}
func TestReleaseMirrorRepairsHistoricalAssetsWithoutMovingNewerPointer(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
root := t.TempDir()
dist := filepath.Join(root, "dist")
seedVersionedReleaseArtifacts(t, dist, "v1.2.3-beta.1")
logPath := filepath.Join(root, "ossutil.log")
fakeOSSUtil := filepath.Join(root, "ossutil")
mustWriteFile(t, fakeOSSUtil, []byte("#!/bin/sh\nset -eu\nprevious=\npenultimate=\nfor arg in \"$@\"; do penultimate=\"$previous\"; previous=\"$arg\"; done\nlast=\"$previous\"\ncase \"$penultimate\" in oss://*) printf 'v1.2.4-beta.1\\n' > \"$last\"; exit 0 ;; esac\nprintf '%s\\n' \"$last\" >> \"$OSSUTIL_LOG\"\n"), 0o755)
cmd := exec.Command("bash", filepath.Join(sourceRoot, "scripts", "release", "sync-to-oss.sh"))
cmd.Dir = sourceRoot
cmd.Env = append(os.Environ(),
"DIST_DIR="+dist,
"VERSION=v1.2.3-beta.1",
"DWS_RELEASE_CHANNEL=prerelease",
"OSS_ACCESS_KEY_ID=test-key",
"OSS_ACCESS_KEY_SECRET=test-secret",
"OSS_ENDPOINT=https://oss.example.com",
"OSS_BUCKET=test-bucket",
"OSS_PREFIX=dws",
"OSSUTIL="+fakeOSSUtil,
"OSSUTIL_LOG="+logPath,
)
output, err := cmd.CombinedOutput()
if err != nil || !strings.Contains(string(output), "assets will be repaired without moving it") {
t.Fatalf("historical OSS repair failed: err=%v\noutput:\n%s", err, output)
}
logData, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile(ossutil log) error = %v", err)
}
if strings.Contains(string(logData), "/beta.txt") {
t.Fatalf("historical repair moved beta pointer:\n%s", logData)
}
if !strings.Contains(string(logData), "/download/v1.2.3-beta.1/") {
t.Fatalf("historical repair did not upload target assets:\n%s", logData)
}
}
func TestReleaseRequiredMirrorsFailWithoutCredentials(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
for _, test := range []struct {
name string
script string
require string
want string
}{
{name: "oss", script: "sync-to-oss.sh", require: "DWS_REQUIRE_OSS=1", want: "OSS mirror sync is required"},
{name: "gitee", script: "sync-to-gitee.sh", require: "DWS_REQUIRE_GITEE=1", want: "Gitee mirror sync is enabled"},
} {
t.Run(test.name, func(t *testing.T) {
cmd := exec.Command("bash", filepath.Join(sourceRoot, "scripts", "release", test.script))
cmd.Dir = sourceRoot
cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + t.TempDir(), test.require}
output, err := cmd.CombinedOutput()
if err == nil || !strings.Contains(string(output), test.want) {
t.Fatalf("required mirror did not fail closed: err=%v, want=%q\noutput:\n%s", err, test.want, output)
}
})
}
}
func TestReleaseArtifactVerificationRequiresEveryChecksum(t *testing.T) {
r := newReleaseTestRepo(t)
dist := t.TempDir()
seedVersionedReleaseArtifacts(t, dist, "v1.2.3")
cmd := exec.Command("sh", r.verify, "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("artifact verification error = %v\noutput:\n%s", err, output)
}
writeReleaseChecksums(t, dist, false)
cmd = exec.Command("sh", r.verify, "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist)
output, err := cmd.CombinedOutput()
if err == nil || !strings.Contains(string(output), "dws-skills.zip exactly once") {
t.Fatalf("missing checksum was not blocked: err=%v\noutput:\n%s", err, output)
}
writeReleaseChecksums(t, dist, true)
mustWriteFile(t, filepath.Join(dist, "dws-linux-riscv64.tar.gz"), []byte("unexpected\n"), 0o644)
cmd = exec.Command("sh", r.verify, "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist)
if output, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(output), "public release assets") {
t.Fatalf("extra public archive was not rejected: err=%v\noutput:\n%s", err, output)
}
if err := os.Remove(filepath.Join(dist, "dws-linux-riscv64.tar.gz")); err != nil {
t.Fatalf("Remove(extra archive) error = %v", err)
}
writeVersionedReleaseArchive(t, dist, "dws-windows-arm64.zip", "v1.2.2")
writeReleaseChecksums(t, dist, true)
cmd = exec.Command("sh", r.verify, "v1.2.3")
cmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+dist)
if output, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(output), "dws-windows-arm64.zip binary") {
t.Fatalf("mixed-version archive was not rejected: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseCommandValidatesThenPushesAnnotatedTag(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "install release automation")
output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"),
"prerelease", "v1.0.1-beta.1", "--remote", "origin",
)
if err != nil {
t.Fatalf("release validation error = %v\noutput:\n%s", err, output)
}
if !strings.Contains(output, "No tag was created") {
t.Fatalf("validation output missing dry-run result:\n%s", output)
}
if mustOutput(t, r.root, "git", "tag", "--list", "v1.0.1-beta.1") != "" {
t.Fatal("validation-only release created a tag")
}
output, err = runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"),
"prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", "--yes",
)
if err != nil {
t.Fatalf("release publish error = %v\noutput:\n%s", err, output)
}
if got := strings.TrimSpace(mustOutput(t, r.root, "git", "cat-file", "-t", "v1.0.1-beta.1")); got != "tag" {
t.Fatalf("release tag type = %q, want annotated tag", got)
}
if got := mustOutput(t, r.root, "git", "ls-remote", "--tags", "origin", "refs/tags/v1.0.1-beta.1"); !strings.Contains(got, "refs/tags/v1.0.1-beta.1") {
t.Fatalf("remote release tag missing:\n%s", got)
}
}
func TestReleaseCommandReusesExactPreflightProof(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
mustWriteFile(t, filepath.Join(r.root, ".gitignore"), []byte("/dws\n"), 0o644)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte(`test:
@rm -f dws
@printf 'test\n' >> "$$(git rev-parse --git-path release-phases)"
build:
@printf '#!/bin/sh\nexit 0\n' > dws
@chmod +x dws
@printf 'build\n' >> "$$(git rev-parse --git-path release-phases)"
policy:
@test -x dws
@printf 'policy\n' >> "$$(git rev-parse --git-path release-phases)"
package:
@printf 'package\n' >> "$$(git rev-parse --git-path release-phases)"
`), 0o644)
r.commitAndPush(t, "install proof-aware release automation")
command := filepath.Join(r.root, "scripts", "release", "release.sh")
output, err := runReleaseScript(t, r.root, command,
"prerelease", "v1.0.1-beta.1", "--remote", "origin",
)
if err != nil {
t.Fatalf("release validation error = %v\noutput:\n%s", err, output)
}
if !strings.Contains(output, "Publish within six hours") {
t.Fatalf("validation output missing reusable-proof guidance:\n%s", output)
}
phasePath := filepath.Join(r.root, ".git", "release-phases")
phaseData, err := os.ReadFile(phasePath)
if err != nil {
t.Fatalf("ReadFile(release phases) error = %v", err)
}
wantPhases := "test\nbuild\npolicy\npackage\n"
if string(phaseData) != wantPhases {
t.Fatalf("release phase order = %q, want %q", phaseData, wantPhases)
}
proofPath := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "--git-path", "dws-release-preflight/v1.0.1-beta.1.proof"))
if !filepath.IsAbs(proofPath) {
proofPath = filepath.Join(r.root, proofPath)
}
proofBefore, err := os.ReadFile(proofPath)
if err != nil {
t.Fatalf("ReadFile(preflight proof) error = %v", err)
}
hook := filepath.Join(r.remote, "hooks", "pre-receive")
mustWriteFile(t, hook, []byte("#!/bin/sh\nset -eu\nwhile read -r old new ref; do\n case \"$ref\" in refs/tags/*) exit 1 ;; esac\ndone\nexit 0\n"), 0o755)
failedOutput, failedErr := runReleaseScript(t, r.root, command,
"prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", "--yes",
)
if failedErr == nil || !strings.Contains(failedOutput, "new local tag was removed") {
t.Fatalf("rejected proof-based publish did not fail safely: err=%v\noutput:\n%s", failedErr, failedOutput)
}
proofAfterFailure, err := os.ReadFile(proofPath)
if err != nil {
t.Fatalf("ReadFile(preflight proof after failure) error = %v", err)
}
if string(proofAfterFailure) != string(proofBefore) {
t.Fatalf("failed publish refreshed reusable proof:\nbefore:\n%s\nafter:\n%s", proofBefore, proofAfterFailure)
}
if err := os.Remove(hook); err != nil {
t.Fatalf("Remove(rejecting hook) error = %v", err)
}
output, err = runReleaseScript(t, r.root, command,
"prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", "--yes",
)
if err != nil {
t.Fatalf("release publish error = %v\noutput:\n%s", err, output)
}
if !strings.Contains(output, "Reusing exact preflight proof") {
t.Fatalf("publish did not reuse the exact preflight proof:\n%s", output)
}
phaseData, err = os.ReadFile(phasePath)
if err != nil {
t.Fatalf("ReadFile(release phases after publish) error = %v", err)
}
if string(phaseData) != wantPhases {
t.Fatalf("publish reran expensive phases: %q", phaseData)
}
}
func TestReleaseGovernanceCIDispatchBindsExactRunIdentity(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
binDir := t.TempDir()
stateDir := t.TempDir()
fakeGH := filepath.Join(binDir, "gh")
mustWriteFile(t, fakeGH, []byte(`#!/bin/sh
set -eu
command="$1"
shift
case "$command" in
workflow)
test "$1" = run
shift
printf '%s\n' "$@" > "$GH_STATE/workflow-args"
for argument in "$@"; do
case "$argument" in
governance_preflight_nonce=*)
printf 'Release governance preflight %s\n' "${argument#governance_preflight_nonce=}" > "$GH_STATE/title"
;;
governance_preflight_commit=*)
printf '%s\n' "${argument#governance_preflight_commit=}" > "$GH_STATE/commit"
;;
esac
done
;;
api)
for argument in "$@"; do
case "$argument" in repos/*/actions/*) endpoint="$argument" ;; esac
done
case "$endpoint" in
*/actions/workflows/release.yml/runs*) printf '42\n' ;;
*/actions/runs/42)
title="$(cat "$GH_STATE/title")"
commit="$(cat "$GH_STATE/commit")"
repository=owner/repo
if [ "${GH_BAD_STATE:-0}" = 1 ]; then repository=attacker/repo; fi
printf '%s\tworkflow_dispatch\tcompleted\tsuccess\tmain\t%s\t.github/workflows/release.yml\t%s\n' "$title" "$commit" "$repository"
;;
*) exit 1 ;;
esac
;;
run)
test "$1" = watch
;;
*) exit 1 ;;
esac
`), 0o755)
script := filepath.Join(sourceRoot, "scripts", "release", "verify-release-governance-ci.sh")
commit := strings.Repeat("a", 40)
run := func(badState bool) (string, error) {
cmd := exec.Command("sh", script, "owner/repo", commit)
cmd.Env = []string{
"PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
"HOME=" + t.TempDir(),
"GH_STATE=" + stateDir,
fmt.Sprintf("GH_BAD_STATE=%d", map[bool]int{false: 0, true: 1}[badState]),
}
output, err := cmd.CombinedOutput()
return string(output), err
}
output, err := run(false)
if err != nil || !strings.Contains(output, "Release governance preflight passed") {
t.Fatalf("governance CI preflight error = %v\noutput:\n%s", err, output)
}
args, err := os.ReadFile(filepath.Join(stateDir, "workflow-args"))
if err != nil {
t.Fatalf("ReadFile(workflow args) error = %v", err)
}
for _, required := range []string{
"--ref\nmain\n",
"governance_preflight_commit=" + commit,
"governance_preflight_nonce=" + commit + "-",
} {
if !strings.Contains(string(args), required) {
t.Errorf("workflow dispatch args are missing %q:\n%s", required, args)
}
}
output, err = run(true)
if err == nil || !strings.Contains(output, "run identity mismatch") {
t.Fatalf("mismatched governance run identity passed: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseCommandCleansLocalTagWhenPushFails(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "install release automation")
hook := filepath.Join(r.remote, "hooks", "pre-receive")
mustWriteFile(t, hook, []byte("#!/bin/sh\nset -eu\nwhile read -r old new ref; do\n case \"$ref\" in refs/tags/*) exit 1 ;; esac\ndone\nexit 0\n"), 0o755)
output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"),
"prerelease", "v1.0.1-beta.1", "--remote", "origin", "--publish", "--yes",
)
if err == nil {
t.Fatalf("rejected tag push unexpectedly passed:\n%s", output)
}
if !strings.Contains(output, "new local tag was removed") {
t.Fatalf("push failure output missing cleanup result:\n%s", output)
}
if mustOutput(t, r.root, "git", "tag", "--list", "v1.0.1-beta.1") != "" {
t.Fatal("failed push left the local release tag behind")
}
}
func TestReleaseCommandRejectsDifferentFetchAndPushRepositories(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "install release automation")
otherRemote := filepath.Join(t.TempDir(), "other.git")
mustRun(t, r.root, "git", "init", "--bare", otherRemote)
mustRun(t, r.root, "git", "remote", "set-url", "--push", "origin", otherRemote)
output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"),
"prerelease", "v1.0.1-beta.1", "--remote", "origin",
)
if err == nil || !strings.Contains(output, "fetch and push URLs target different repositories") {
t.Fatalf("split release authority was not rejected: err=%v\noutput:\n%s", err, output)
}
}
func TestReleaseCommandRechecksAdvancedStableAuthority(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
section := "## [1.0.2-beta.1] - 2026-07-11\n\n### Changed\n\n- Validate a candidate after stable authority advances.\n\n"
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(section)), 0o644)
mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-command-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nprintf 'compatibility %s\\n' \"$*\"\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte("test:\n\t@:\nbuild:\n\t@:\npolicy:\n\t@:\npackage:\n\t@git tag -a v1.0.1 -m 'Release v1.0.1'\n\t@git push origin refs/tags/v1.0.1\n"), 0o644)
r.commitAndPush(t, "install advancing release fixture")
output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"),
"prerelease", "v1.0.2-beta.1", "--remote", "origin",
)
if err != nil {
t.Fatalf("release validation error = %v\noutput:\n%s", err, output)
}
if !strings.Contains(output, "Stable authority advanced from v1.0.0 to v1.0.1") ||
!strings.Contains(output, "--stable-ref v1.0.1") {
t.Fatalf("advanced stable command tree was not rechecked:\n%s", output)
}
}
func installReleaseCommandFixture(t *testing.T, r *releaseTestRepo) {
t.Helper()
for _, source := range []string{r.lib, r.contract, r.releaseCmd} {
releaseCopyFile(t, source, filepath.Join(r.root, "scripts", "release", filepath.Base(source)), 0o755)
}
mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "verify-package-managers.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "verify-release-artifacts.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-command-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte("test:\n\t@:\nbuild:\n\t@:\npolicy:\n\t@:\npackage:\n\t@:\n"), 0o644)
}
func releaseCopyFile(t *testing.T, source, target string, mode os.FileMode) {
t.Helper()
data, err := os.ReadFile(source)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", source, err)
}
mustWriteFile(t, target, data, mode)
}