feat(release): add guarded prerelease and stable pipeline (#54)

* feat(release): add guarded prerelease and stable pipeline

* feat(release): add guided dws-release entry

(cherry picked from commit f7fa7b78f325f3574f0487862fc3e65bba5cdc96)
This commit is contained in:
修雨
2026-07-16 10:26:18 +08:00
committed by 修雨
parent e615bd433c
commit 22d19863fb
32 changed files with 4327 additions and 591 deletions
+18 -13
View File
@@ -105,6 +105,8 @@ func seedDistArchive(t *testing.T, path string) {
func postGoreleaserEnv(t *testing.T, distDir, releaseBaseURL string) []string {
t.Helper()
trimmedBaseURL := strings.TrimRight(releaseBaseURL, "/")
version := trimmedBaseURL[strings.LastIndex(trimmedBaseURL, "/")+1:]
binDir := t.TempDir()
fakeCodesign := filepath.Join(binDir, "codesign")
@@ -114,7 +116,7 @@ func postGoreleaserEnv(t *testing.T, distDir, releaseBaseURL string) []string {
return append(os.Environ(),
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
"DWS_PACKAGE_VERSION=v0.0.0",
"DWS_PACKAGE_VERSION="+version,
"DWS_PACKAGE_DIST_DIR="+distDir,
"DWS_RELEASE_BASE_URL="+releaseBaseURL,
)
@@ -186,6 +188,8 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) {
formulaText := string(formulaData)
for _, want := range []string{
"class DingtalkWorkspaceCliLocal < Formula",
"version \"1.2.3\"",
"skill_home_override = \"\"",
"resource \"skills\" do",
"Install locally built DingTalk workspace CLI artifacts for verification",
} {
@@ -202,14 +206,8 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) {
releaseFormulaText := string(releaseFormulaData)
for _, want := range []string{
"class DingtalkWorkspaceCli < Formula",
`desc "Automate DingTalk workspace tasks from the terminal"`,
`version "0.0.0"`,
"on_macos do",
"on_linux do",
"https://downloads.example.com/dws/releases/v1.2.3/dws-darwin-amd64.tar.gz",
"https://downloads.example.com/dws/releases/v1.2.3/dws-darwin-arm64.tar.gz",
"https://downloads.example.com/dws/releases/v1.2.3/dws-linux-amd64.tar.gz",
"https://downloads.example.com/dws/releases/v1.2.3/dws-linux-arm64.tar.gz",
"version \"1.2.3\"",
"https://downloads.example.com/dws/releases/v1.2.3/" + archiveName,
"https://downloads.example.com/dws/releases/v1.2.3/dws-skills.zip",
} {
if !strings.Contains(releaseFormulaText, want) {
@@ -259,13 +257,20 @@ func TestPostGoreleaserBuildsExpectedArtifacts(t *testing.T) {
}
}
// Verify checksums.txt was updated to include skills zip
// Re-running post packaging must replace, not duplicate, the skills checksum.
cmd = exec.Command("sh", scriptPath)
cmd.Env = postGoreleaserEnv(t, distDir, "https://downloads.example.com/dws/releases/v1.2.3")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("second post-goreleaser.sh error = %v\noutput:\n%s", err, output)
}
// Verify checksums.txt includes exactly one skills zip entry.
checksumsData, err := os.ReadFile(filepath.Join(distDir, "checksums.txt"))
if err != nil {
t.Fatalf("ReadFile(checksums.txt) error = %v", err)
}
if !strings.Contains(string(checksumsData), "dws-skills.zip") {
t.Fatalf("checksums.txt missing dws-skills.zip entry:\n%s", string(checksumsData))
if count := strings.Count(string(checksumsData), "dws-skills.zip"); count != 1 {
t.Fatalf("checksums.txt dws-skills.zip count = %d, want 1:\n%s", count, checksumsData)
}
}
@@ -449,7 +454,7 @@ func TestPostGoreleaserAllPlatformNpmAssets(t *testing.T) {
}
packageAssetsDir := filepath.Join(distDir, "npm", "dingtalk-workspace-cli", "assets")
for _, rel := range append(allArchives, "dws-skills.zip") {
for _, rel := range append(allArchives, "dws-skills.zip", "checksums.txt") {
if _, err := os.Stat(filepath.Join(packageAssetsDir, rel)); err != nil {
t.Fatalf("npm asset missing %q: %v", rel, err)
}
+301
View File
@@ -0,0 +1,301 @@
package scripts_test
import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
type releaseEntryFixture struct {
repo *releaseTestRepo
entry string
log string
}
func newReleaseEntryFixture(t *testing.T) *releaseEntryFixture {
t.Helper()
r := newReleaseTestRepo(t)
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
entry := filepath.Join(r.root, "scripts", "release", "dws-release.sh")
releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644)
releaseCopyFile(t, filepath.Join(sourceRoot, "scripts", "release", "dws-release.sh"), entry, 0o755)
fakeDelegate := func(name string) []byte {
return []byte("#!/bin/sh\nset -eu\nprintf '" + name + "' >> \"$DWS_RELEASE_CALL_LOG\"\nfor arg in \"$@\"; do printf '\\t%s' \"$arg\" >> \"$DWS_RELEASE_CALL_LOG\"; done\nprintf '\\n' >> \"$DWS_RELEASE_CALL_LOG\"\nexit \"${DWS_RELEASE_FAKE_EXIT:-0}\"\n")
}
mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), fakeDelegate("prepare"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "release.sh"), fakeDelegate("release"), 0o755)
r.commitAndPush(t, "install unified release entry fixture")
return &releaseEntryFixture{repo: r, entry: entry, log: filepath.Join(t.TempDir(), "calls.log")}
}
func (f *releaseEntryFixture) run(t *testing.T, extraEnv []string, args ...string) (string, error) {
t.Helper()
cmd := exec.Command("sh", append([]string{f.entry}, args...)...)
cmd.Dir = f.repo.root
cmd.Env = append(os.Environ(), "DWS_RELEASE_CALL_LOG="+f.log)
cmd.Env = append(cmd.Env, extraEnv...)
output, err := cmd.CombinedOutput()
return string(output), err
}
func (f *releaseEntryFixture) configureRemote(t *testing.T, remote string) {
t.Helper()
output, err := f.run(t, nil, "config", "--remote", remote)
if err != nil {
t.Fatalf("config remote error = %v\noutput:\n%s", err, output)
}
}
func (f *releaseEntryFixture) callLog(t *testing.T) string {
t.Helper()
data, err := os.ReadFile(f.log)
if errors.Is(err, os.ErrNotExist) {
return ""
}
if err != nil {
t.Fatalf("ReadFile(call log) error = %v", err)
}
return string(data)
}
func TestDWSReleaseEntryPreparesMissingChangelogAndStops(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{
name: "prerelease",
args: []string{"v1.0.1-beta.1", "--remote", "origin", "--publish"},
want: "prepare\tprerelease\tv1.0.1-beta.1\n",
},
{
name: "stable",
args: []string{"v1.0.1", "--from-beta", "v1.0.1-beta.1", "--remote", "origin"},
want: "prepare\tstable\tv1.0.1\t--from-beta\tv1.0.1-beta.1\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := newReleaseEntryFixture(t)
f.configureRemote(t, "origin")
output, err := f.run(t, nil, test.args...)
if err != nil {
t.Fatalf("dws-release prepare error = %v\noutput:\n%s", err, output)
}
if got := f.callLog(t); got != test.want {
t.Fatalf("delegate calls = %q, want %q", got, test.want)
}
if !strings.Contains(output, "publishing intentionally stopped") {
t.Fatalf("prepare output did not make the stop boundary clear:\n%s", output)
}
})
}
}
func TestDWSReleaseEntryRoutesOneGuardedCommand(t *testing.T) {
tests := []struct {
name string
section string
args []string
wantCall string
}{
{
name: "check prerelease",
section: betaSection(),
args: []string{"v1.0.1-beta.1", "--remote", "origin"},
wantCall: "release\tprerelease\tv1.0.1-beta.1\t--remote\torigin\n",
},
{
name: "publish prerelease keeps confirmation",
section: betaSection(),
args: []string{"v1.0.1-beta.1", "--remote", "origin", "--publish"},
wantCall: "release\tprerelease\tv1.0.1-beta.1\t--remote\torigin\t--publish\n",
},
{
name: "check stable",
section: stableSection(),
args: []string{"v1.0.1", "--from-beta", "v1.0.1-beta.1", "--remote", "origin"},
wantCall: "release\tstable\tv1.0.1\t--remote\torigin\t--from-beta\tv1.0.1-beta.1\n",
},
{
name: "publish stable keeps confirmation",
section: stableSection(),
args: []string{"v1.0.1", "--from-beta", "v1.0.1-beta.1", "--remote", "origin", "--publish"},
wantCall: "release\tstable\tv1.0.1\t--remote\torigin\t--from-beta\tv1.0.1-beta.1\t--publish\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := newReleaseEntryFixture(t)
f.configureRemote(t, "origin")
mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(test.section)), 0o644)
f.repo.commitAndPush(t, "add candidate changelog")
output, err := f.run(t, nil, test.args...)
if err != nil {
t.Fatalf("dws-release route error = %v\noutput:\n%s", err, output)
}
if got := f.callLog(t); got != test.wantCall {
t.Fatalf("delegate calls = %q, want %q\noutput:\n%s", got, test.wantCall, output)
}
})
}
}
func TestDWSReleaseEntryRejectsInvalidInvocationBeforeDelegation(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{name: "invalid version", args: []string{"1.0.1"}, want: "invalid release version"},
{name: "stable missing beta", args: []string{"v1.0.1"}, want: "requires --from-beta"},
{name: "prerelease with beta", args: []string{"v1.0.1-beta.1", "--from-beta", "v1.0.1-beta.1"}, want: "only valid for stable"},
{name: "conflicting mode", args: []string{"v1.0.1-beta.1", "--check", "--publish"}, want: "cannot be used together"},
{name: "yes bypass", args: []string{"v1.0.1-beta.1", "--publish", "--yes"}, want: "unknown argument"},
{name: "unknown flag", args: []string{"v1.0.1-beta.1", "--force"}, want: "unknown argument"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := newReleaseEntryFixture(t)
output, err := f.run(t, nil, test.args...)
if err == nil || !strings.Contains(output, test.want) {
t.Fatalf("invalid invocation: err=%v, want=%q\noutput:\n%s", err, test.want, output)
}
if got := f.callLog(t); got != "" {
t.Fatalf("invalid invocation delegated work: %q", got)
}
})
}
}
func TestDWSReleaseEntryUsesConfiguredRemote(t *testing.T) {
f := newReleaseEntryFixture(t)
f.configureRemote(t, "origin")
mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
f.repo.commitAndPush(t, "add beta changelog")
output, err := f.run(t, nil, "v1.0.1-beta.1")
if err != nil {
t.Fatalf("configured remote route error = %v\noutput:\n%s", err, output)
}
want := "release\tprerelease\tv1.0.1-beta.1\t--remote\torigin\n"
if got := f.callLog(t); got != want {
t.Fatalf("delegate calls = %q, want %q", got, want)
}
}
func TestDWSReleaseEntryFailsClosedWithoutRemote(t *testing.T) {
f := newReleaseEntryFixture(t)
mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
f.repo.commitAndPush(t, "add beta changelog")
output, err := f.run(t, nil, "v1.0.1-beta.1")
if err == nil || !strings.Contains(output, "release remote is not configured") {
t.Fatalf("missing remote did not fail closed: err=%v\noutput:\n%s", err, output)
}
if got := f.callLog(t); got != "" {
t.Fatalf("missing remote delegated work: %q", got)
}
}
func TestDWSReleaseEntryPropagatesDelegateFailure(t *testing.T) {
f := newReleaseEntryFixture(t)
f.configureRemote(t, "origin")
mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
f.repo.commitAndPush(t, "add beta changelog")
output, err := f.run(t, []string{"DWS_RELEASE_FAKE_EXIT=23"}, "v1.0.1-beta.1", "--remote", "origin")
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) || exitErr.ExitCode() != 23 {
t.Fatalf("delegate error was not propagated: err=%v\noutput:\n%s", err, output)
}
}
func TestDWSReleaseEntryFastForwardsCleanMain(t *testing.T) {
f := newReleaseEntryFixture(t)
f.configureRemote(t, "origin")
other := filepath.Join(t.TempDir(), "other")
mustRun(t, filepath.Dir(other), "git", "clone", "-b", "main", f.repo.remote, other)
mustRun(t, other, "git", "config", "user.name", "Release Test")
mustRun(t, other, "git", "config", "user.email", "release-test@example.com")
mustWriteFile(t, filepath.Join(other, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
mustRun(t, other, "git", "add", "CHANGELOG.md")
mustRun(t, other, "git", "commit", "-m", "add remote beta changelog")
mustRun(t, other, "git", "push", "origin", "main")
output, err := f.run(t, nil, "v1.0.1-beta.1", "--remote", "origin")
if err != nil {
t.Fatalf("fast-forward route error = %v\noutput:\n%s", err, output)
}
localHead := strings.TrimSpace(mustOutput(t, f.repo.root, "git", "rev-parse", "HEAD"))
remoteHead := strings.TrimSpace(mustOutput(t, f.repo.root, "git", "rev-parse", "refs/remotes/origin/main"))
if localHead != remoteHead {
t.Fatalf("local main was not fast-forwarded: local=%s remote=%s", localHead, remoteHead)
}
if got := f.callLog(t); !strings.HasPrefix(got, "release\tprerelease\tv1.0.1-beta.1") {
t.Fatalf("fast-forward did not reach guarded delegate: %q", got)
}
}
func TestDWSReleaseEntryRejectsRetargetedRemote(t *testing.T) {
f := newReleaseEntryFixture(t)
f.configureRemote(t, "origin")
mustWriteFile(t, filepath.Join(f.repo.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
f.repo.commitAndPush(t, "add beta changelog")
otherRemote := filepath.Join(t.TempDir(), "other.git")
mustRun(t, f.repo.root, "git", "init", "--bare", otherRemote)
mustRun(t, f.repo.root, "git", "remote", "set-url", "origin", otherRemote)
output, err := f.run(t, nil, "v1.0.1-beta.1")
if err == nil || !strings.Contains(output, "changed repository identity") {
t.Fatalf("retargeted remote was not rejected: err=%v\noutput:\n%s", err, output)
}
if got := f.callLog(t); got != "" {
t.Fatalf("retargeted remote delegated work: %q", got)
}
}
func TestDWSReleaseEntryRejectsAuthorityOverrides(t *testing.T) {
tests := []struct {
name string
env string
want string
}{
{name: "non github remote", env: "DWS_RELEASE_ALLOW_NON_GITHUB_REMOTE=1", want: "test-only"},
{name: "official tags URL", env: "DWS_RELEASE_OFFICIAL_TAGS_URL=https://example.com/tags.git", want: "cannot override"},
{name: "official repository", env: "DWS_RELEASE_OFFICIAL_REPOSITORY=other/repo", want: "cannot override"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := newReleaseEntryFixture(t)
output, err := f.run(t, []string{test.env}, "v1.0.1-beta.1")
if err == nil || !strings.Contains(output, test.want) {
t.Fatalf("authority override was not rejected: err=%v, want=%q\noutput:\n%s", err, test.want, output)
}
if got := f.callLog(t); got != "" {
t.Fatalf("authority override delegated work: %q", got)
}
})
}
}
func TestReleaseCommandRejectsNonInteractivePublishWithoutYes(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", "--publish",
)
if err == nil || !strings.Contains(output, "interactive confirmation is unavailable") {
t.Fatalf("non-interactive publish did not require confirmation: err=%v\noutput:\n%s", err, output)
}
if got := mustOutput(t, r.root, "git", "tag", "--list", "v1.0.1-beta.1"); got != "" {
t.Fatalf("rejected non-interactive publish created a tag: %s", got)
}
}
+924
View File
@@ -0,0 +1,924 @@
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 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 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 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")
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://*) 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,
)
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)
}
})
}
}
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 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@:\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@:\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)
}