Merge branch 'main' into codex/fix-plugin-command-registration
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -45,6 +46,29 @@ var expectedReleaseAdmissionContexts = []string{
|
||||
"Mock MCP",
|
||||
}
|
||||
|
||||
func TestPackageManagerVersionVerificationReadsRawBinary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "verify-package-managers.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(verify-package-managers.sh) error = %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(scriptPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%s) error = %v", scriptPath, err)
|
||||
}
|
||||
script := string(data)
|
||||
for _, binary := range []string{`"$vendor_bin"`, `"$prefix/bin/dws"`} {
|
||||
want := `LC_ALL=C grep -aFq "v$EXPECTED_VERSION" ` + binary
|
||||
if !strings.Contains(script, want) {
|
||||
t.Errorf("package-manager verifier is missing raw binary marker check %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(script, `strings "$vendor_bin"`) || strings.Contains(script, `strings "$prefix/bin/dws"`) {
|
||||
t.Fatal("package-manager verifier still requires the version marker to occupy a strings(1) line")
|
||||
}
|
||||
}
|
||||
|
||||
func seedDistArchive(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
file, err := os.Create(path)
|
||||
@@ -636,6 +660,33 @@ func releaseWorkflowSection(t *testing.T, workflow, startMarker, endMarker strin
|
||||
return workflow[start : start+len(startMarker)+end]
|
||||
}
|
||||
|
||||
func releaseWorkflowRunScript(t *testing.T, workflow, stepName, nextStepName string) string {
|
||||
t.Helper()
|
||||
section := releaseWorkflowSection(
|
||||
t,
|
||||
workflow,
|
||||
" - name: "+stepName+"\n",
|
||||
"\n - name: "+nextStepName+"\n",
|
||||
)
|
||||
const runMarker = " run: |\n"
|
||||
start := strings.Index(section, runMarker)
|
||||
if start == -1 {
|
||||
t.Fatalf("release workflow step %q is missing a run block", stepName)
|
||||
}
|
||||
|
||||
lines := strings.Split(section[start+len(runMarker):], "\n")
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, " ") {
|
||||
t.Fatalf("release workflow step %q has an unexpected run indentation: %q", stepName, line)
|
||||
}
|
||||
lines[i] = strings.TrimPrefix(line, " ")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowUsesDedicatedGovernanceIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
workflow := readReleaseWorkflow(t)
|
||||
@@ -836,7 +887,8 @@ func TestReleaseWorkflowPlansAndSealsCurrentMainInTheCloud(t *testing.T) {
|
||||
"persist-credentials: false",
|
||||
`refs/remotes/origin/main)" = "$GITHUB_SHA`,
|
||||
"next-release-version.sh",
|
||||
"refs/tags/withdrawn/v",
|
||||
`'refs/tags/v*' 'refs/tags/withdrawn/v*'`,
|
||||
"release ref manifest is empty after fetching allocated tags",
|
||||
"refs_fingerprint",
|
||||
"Validate the candidate release contract before sealing",
|
||||
"release-contract.sh",
|
||||
@@ -850,6 +902,9 @@ func TestReleaseWorkflowPlansAndSealsCurrentMainInTheCloud(t *testing.T) {
|
||||
if strings.Contains(plan, "contents: write") {
|
||||
t.Error("cloud release planning must remain read-only")
|
||||
}
|
||||
if strings.Contains(plan, "refs/tags/v refs/tags/withdrawn/v") {
|
||||
t.Error("cloud release planning must use wildcard ref patterns that match the seal API prefixes")
|
||||
}
|
||||
|
||||
for _, required := range []string{
|
||||
"name: Seal cloud release tag",
|
||||
@@ -884,6 +939,136 @@ func TestReleaseWorkflowPlansAndSealsCurrentMainInTheCloud(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseFingerprintRefPatternsMatchAllAllocatedTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
repo := t.TempDir()
|
||||
mustRun(t, repo, "git", "init", "-b", "main")
|
||||
mustRun(t, repo, "git", "config", "user.name", "Release Fingerprint Test")
|
||||
mustRun(t, repo, "git", "config", "user.email", "release-fingerprint@example.com")
|
||||
mustWriteFile(t, filepath.Join(repo, "tracked"), []byte("fixture\n"), 0o644)
|
||||
mustRun(t, repo, "git", "add", "tracked")
|
||||
mustRun(t, repo, "git", "commit", "-m", "fixture")
|
||||
|
||||
allocatedTags := []string{
|
||||
"v1.0.52",
|
||||
"v1.0.53-beta.5",
|
||||
"withdrawn/v1.0.51",
|
||||
}
|
||||
for _, tag := range allocatedTags {
|
||||
mustRun(t, repo, "git", "tag", "-a", tag, "-m", "Release "+tag)
|
||||
}
|
||||
mustRun(t, repo, "git", "tag", "-a", "release/v1.0.52", "-m", "unrelated namespace")
|
||||
legacy := exec.Command(
|
||||
"git", "for-each-ref", "--format=%(refname)=%(objectname)",
|
||||
"refs/tags/v", "refs/tags/withdrawn/v",
|
||||
)
|
||||
legacy.Dir = repo
|
||||
legacyOutput, err := legacy.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("legacy git for-each-ref error = %v\noutput:\n%s", err, legacyOutput)
|
||||
}
|
||||
if strings.TrimSpace(string(legacyOutput)) != "" {
|
||||
t.Fatalf("legacy component patterns unexpectedly matched flat release refs:\n%s", legacyOutput)
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
"git", "for-each-ref", "--format=%(refname)=%(objectname)",
|
||||
"refs/tags/v*", "refs/tags/withdrawn/v*",
|
||||
)
|
||||
cmd.Dir = repo
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git for-each-ref error = %v\noutput:\n%s", err, output)
|
||||
}
|
||||
got := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
sort.Strings(got)
|
||||
|
||||
want := make([]string, 0, len(allocatedTags))
|
||||
for _, tag := range allocatedTags {
|
||||
object := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "refs/tags/"+tag))
|
||||
want = append(want, "refs/tags/"+tag+"="+object)
|
||||
}
|
||||
sort.Strings(want)
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Fatalf("release ref set differs from the seal API set\ngot:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
workflowDigest := sha256.Sum256([]byte(strings.Join(got, "\n") + "\n"))
|
||||
sealDigest := sha256.Sum256([]byte(strings.Join(want, "\n") + "\n"))
|
||||
if workflowDigest != sealDigest {
|
||||
t.Fatalf("release ref fingerprint differs from seal fingerprint: workflow=%x seal=%x", workflowDigest, sealDigest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowAcceptsGuardedLocalTagMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
workflow := readReleaseWorkflow(t)
|
||||
|
||||
for _, required := range []string{
|
||||
`const cloudOnlyKeys = [`,
|
||||
`const cloudKeys = ["Channel", ...cloudOnlyKeys];`,
|
||||
`const hasAnyCloudMetadata = cloudOnlyKeys.some((key) => tagFields.has(key));`,
|
||||
`const isCloudSeal = cloudKeys.every((key) => tagFields.has(key));`,
|
||||
} {
|
||||
if !strings.Contains(workflow, required) {
|
||||
t.Errorf("local tag metadata compatibility is missing %q", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(workflow, `const hasAnyCloudMetadata = cloudKeys.some((key) => tagFields.has(key));`) {
|
||||
t.Error("Channel-only guarded local tags must not be classified as partial cloud seals")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowRequiresOSSOnlyWhenMirrorIsEnabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
workflow := readReleaseWorkflow(t)
|
||||
releaseContract := releaseWorkflowSection(t, workflow, " release-contract:\n", "\n release:\n")
|
||||
targetAuthority := releaseWorkflowSection(
|
||||
t,
|
||||
releaseContract,
|
||||
" - name: Resolve and verify exact release target\n",
|
||||
"\n - name: Check out repository\n",
|
||||
)
|
||||
ossStep := releaseWorkflowSection(
|
||||
t,
|
||||
workflow,
|
||||
" - name: Sync release artifacts to China OSS mirror\n",
|
||||
"\n mirror-gitee-release:\n",
|
||||
)
|
||||
|
||||
for _, required := range []string{
|
||||
`if: ${{ needs.release-contract.outputs.oss_mirror == 'enabled' }}`,
|
||||
`run: ./scripts/release/sync-to-oss.sh`,
|
||||
`DWS_REQUIRE_OSS: "1"`,
|
||||
} {
|
||||
if !strings.Contains(ossStep, required) {
|
||||
t.Errorf("opt-in OSS publication is missing %q", required)
|
||||
}
|
||||
}
|
||||
for _, required := range []string{
|
||||
`OSS_MIRROR: ${{ vars.ENABLE_OSS_MIRROR == 'true' && 'enabled' || 'deferred' }}`,
|
||||
`OSS-Mirror: ${ossMirror}`,
|
||||
`core.setOutput("oss_mirror", ossMirror);`,
|
||||
} {
|
||||
if !strings.Contains(workflow, required) {
|
||||
t.Errorf("immutable OSS release policy is missing %q", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(ossStep, "vars.ENABLE_OSS_MIRROR") {
|
||||
t.Error("channel publication must use the immutable tag policy, not the current repository variable")
|
||||
}
|
||||
for _, required := range []string{
|
||||
`const ossMirror = tagFields.has("OSS-Mirror")`,
|
||||
`? tagFields.get("OSS-Mirror")`,
|
||||
`: "enabled";`,
|
||||
`!["enabled", "deferred"].includes(ossMirror)`,
|
||||
`core.setOutput("oss_mirror", ossMirror);`,
|
||||
} {
|
||||
if !strings.Contains(targetAuthority, required) {
|
||||
t.Errorf("release target OSS policy authority is missing %q", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowChannelRepairUsesSealedReleaseAuthority(t *testing.T) {
|
||||
t.Parallel()
|
||||
workflow := readReleaseWorkflow(t)
|
||||
@@ -897,6 +1082,12 @@ func TestReleaseWorkflowChannelRepairUsesSealedReleaseAuthority(t *testing.T) {
|
||||
t.Fatal("release workflow channel repair job is missing its end marker")
|
||||
}
|
||||
repair := workflow[start : start+end]
|
||||
authority := releaseWorkflowSection(
|
||||
t,
|
||||
repair,
|
||||
" - name: Verify immutable release authority\n",
|
||||
"\n - name: Require sealed OSS policy for repair\n",
|
||||
)
|
||||
tagAuthority := releaseWorkflowSection(
|
||||
t,
|
||||
repair,
|
||||
@@ -945,6 +1136,8 @@ func TestReleaseWorkflowChannelRepairUsesSealedReleaseAuthority(t *testing.T) {
|
||||
"assetNames.length !== expectedAssets.size",
|
||||
"new Set(assetNames).size !== expectedAssets.size",
|
||||
`core.setOutput("tag_object", ref.data.object.sha)`,
|
||||
"Require sealed OSS policy for repair",
|
||||
"OSS repair is unavailable because this immutable release deferred the OSS channel.",
|
||||
`ref: ${{ steps.authority.outputs.commit_sha }}`,
|
||||
"path: release-source",
|
||||
"verify-github-tag-authority.sh",
|
||||
@@ -975,6 +1168,25 @@ func TestReleaseWorkflowChannelRepairUsesSealedReleaseAuthority(t *testing.T) {
|
||||
t.Errorf("channel repair authority is missing %q", required)
|
||||
}
|
||||
}
|
||||
for _, required := range []string{
|
||||
`const tagFields = new Map();`,
|
||||
`const ossMirror = tagFields.has("OSS-Mirror")`,
|
||||
`? tagFields.get("OSS-Mirror")`,
|
||||
`: "enabled";`,
|
||||
`!["enabled", "deferred"].includes(ossMirror)`,
|
||||
`core.setOutput("oss_mirror", ossMirror);`,
|
||||
} {
|
||||
if !strings.Contains(authority, required) {
|
||||
t.Errorf("channel repair tag policy authority is missing %q", required)
|
||||
}
|
||||
}
|
||||
npmRepair := releaseWorkflowSection(t, workflow, " repair-npm:\n", "\n release-delivery-gate:\n")
|
||||
if strings.Contains(npmRepair, `const ossMirror`) || strings.Contains(npmRepair, `core.setOutput("oss_mirror"`) {
|
||||
t.Error("npm repair must not parse or export the channel-only OSS policy")
|
||||
}
|
||||
if strings.Contains(repair, "ENABLE_OSS_MIRROR") {
|
||||
t.Error("OSS repair must use the immutable tag policy, not the current repository variable")
|
||||
}
|
||||
for _, asset := range []string{
|
||||
"dws-darwin-amd64.tar.gz",
|
||||
"dws-darwin-arm64.tar.gz",
|
||||
@@ -1632,6 +1844,133 @@ func TestReleaseWorkflowOpensVersionedHomebrewPRForBetaTags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowWaitsForNPMDistTagPropagation(t *testing.T) {
|
||||
workflow := readReleaseWorkflow(t)
|
||||
script := releaseWorkflowRunScript(
|
||||
t,
|
||||
workflow,
|
||||
"Verify npm channel delivery",
|
||||
"Sync release artifacts to China OSS mirror",
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sequence string
|
||||
wantSuccess bool
|
||||
wantCalls int
|
||||
wantSleeps int
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
name: "stale beta converges to target",
|
||||
sequence: "1.0.53-beta.6\n1.0.53-beta.6\n1.0.53-beta.7\n",
|
||||
wantSuccess: true,
|
||||
wantCalls: 3,
|
||||
wantSleeps: 2,
|
||||
},
|
||||
{
|
||||
name: "stale beta never converges",
|
||||
sequence: "1.0.53-beta.6\n",
|
||||
wantCalls: 12,
|
||||
wantSleeps: 11,
|
||||
wantOutput: "still reports older v1.0.53-beta.6 after 12 attempts",
|
||||
},
|
||||
{
|
||||
name: "permanent registry error fails immediately",
|
||||
sequence: "__NPM_ERROR__\n",
|
||||
wantCalls: 1,
|
||||
wantSleeps: 0,
|
||||
wantOutput: "permanent npm registry error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fakeBin := filepath.Join(root, "bin")
|
||||
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(%s) error = %v", fakeBin, err)
|
||||
}
|
||||
sequencePath := filepath.Join(root, "sequence")
|
||||
statePath := filepath.Join(root, "state")
|
||||
npmLogPath := filepath.Join(root, "npm.log")
|
||||
sleepLogPath := filepath.Join(root, "sleep.log")
|
||||
mustWriteFile(t, sequencePath, []byte(test.sequence), 0o644)
|
||||
mustWriteFile(t, filepath.Join(fakeBin, "npm"), []byte(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%s\n' "$*" >> "$NPM_CALL_LOG"
|
||||
test "$*" = "view dingtalk-workspace-cli dist-tags.beta --registry=https://registry.npmjs.org --prefer-online" || {
|
||||
echo "unexpected npm mutation: $*" >&2
|
||||
exit 97
|
||||
}
|
||||
call=0
|
||||
if test -f "$NPM_STATE"; then call="$(cat "$NPM_STATE")"; fi
|
||||
call=$((call + 1))
|
||||
printf '%s\n' "$call" > "$NPM_STATE"
|
||||
value="$(sed -n "${call}p" "$NPM_SEQUENCE")"
|
||||
if test -z "$value"; then value="$(tail -n 1 "$NPM_SEQUENCE")"; fi
|
||||
if test "$value" = "__NPM_ERROR__"; then
|
||||
echo "permanent npm registry error" >&2
|
||||
exit 42
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
`), 0o755)
|
||||
mustWriteFile(t, filepath.Join(fakeBin, "sleep"), []byte(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%s\n' "$*" >> "$SLEEP_CALL_LOG"
|
||||
`), 0o755)
|
||||
|
||||
repoRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(repository root) error = %v", err)
|
||||
}
|
||||
cmd := exec.Command("sh", "-c", script)
|
||||
cmd.Dir = repoRoot
|
||||
cmd.Env = append(os.Environ(),
|
||||
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
|
||||
"NPM_TAG=beta",
|
||||
"SEMVER=1.0.53-beta.7",
|
||||
"NPM_SEQUENCE="+sequencePath,
|
||||
"NPM_STATE="+statePath,
|
||||
"NPM_CALL_LOG="+npmLogPath,
|
||||
"SLEEP_CALL_LOG="+sleepLogPath,
|
||||
)
|
||||
output, runErr := cmd.CombinedOutput()
|
||||
if test.wantSuccess && runErr != nil {
|
||||
t.Fatalf("npm delivery verification error = %v\noutput:\n%s", runErr, output)
|
||||
}
|
||||
if !test.wantSuccess && runErr == nil {
|
||||
t.Fatalf("npm delivery verification unexpectedly succeeded\noutput:\n%s", output)
|
||||
}
|
||||
if test.wantOutput != "" && !strings.Contains(string(output), test.wantOutput) {
|
||||
t.Errorf("npm delivery verification output is missing %q:\n%s", test.wantOutput, output)
|
||||
}
|
||||
|
||||
npmLog, err := os.ReadFile(npmLogPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%s) error = %v", npmLogPath, err)
|
||||
}
|
||||
npmCalls := strings.Split(strings.TrimSpace(string(npmLog)), "\n")
|
||||
if got := len(npmCalls); got != test.wantCalls {
|
||||
t.Errorf("npm view call count = %d, want %d; log:\n%s", got, test.wantCalls, npmLog)
|
||||
}
|
||||
if strings.Contains(string(npmLog), "dist-tag add") || strings.Contains(string(npmLog), "publish") {
|
||||
t.Errorf("delivery verification must remain read-only; log:\n%s", npmLog)
|
||||
}
|
||||
|
||||
sleepCalls := 0
|
||||
if sleepLog, err := os.ReadFile(sleepLogPath); err == nil {
|
||||
sleepCalls = len(strings.Fields(string(sleepLog)))
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Fatalf("ReadFile(%s) error = %v", sleepLogPath, err)
|
||||
}
|
||||
if sleepCalls != test.wantSleeps {
|
||||
t.Errorf("sleep call count = %d, want %d", sleepCalls, test.wantSleeps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseStaysDraftUntilFinalizedAssetDigestsMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -943,6 +943,11 @@ esac
|
||||
if output, err := run(); err != nil || !strings.Contains(output, "cloud release run 42") {
|
||||
t.Fatalf("tag-bound cloud delivery was rejected: err=%v\noutput:\n%s", err, output)
|
||||
}
|
||||
if output, err := runWithArgs(
|
||||
[]string{"--channel-repair", "oss", tag, commit},
|
||||
); err != nil || !strings.Contains(output, "cloud release run 42") {
|
||||
t.Fatalf("OSS repair could not use a successful release with the mirror enabled: err=%v\noutput:\n%s", err, output)
|
||||
}
|
||||
if output, err := run("RUN_ACTOR=renamed-release-user"); err != nil ||
|
||||
!strings.Contains(output, "cloud release run 42") {
|
||||
t.Fatalf("cloud delivery broke after a harmless login rename: err=%v\noutput:\n%s", err, output)
|
||||
@@ -1489,12 +1494,12 @@ func TestReleaseContractRejectsDirtyOrUnsyncedMain(t *testing.T) {
|
||||
"--version", "v1.0.1-beta.1",
|
||||
"--remote", "origin",
|
||||
)
|
||||
if err == nil || !strings.Contains(output, "must exactly match origin/main") {
|
||||
if err == nil || !strings.Contains(output, "must be contained in origin/main history") {
|
||||
t.Fatalf("unsynced main was not blocked: err=%v\noutput:\n%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseContractStablePromotionAllowsOnlyChangelogDiff(t *testing.T) {
|
||||
func TestReleaseContractStablePromotionRequiresBetaAncestry(t *testing.T) {
|
||||
t.Run("sealed", func(t *testing.T) {
|
||||
r := newReleaseTestRepo(t)
|
||||
r.seedBeta(t)
|
||||
@@ -1513,12 +1518,12 @@ func TestReleaseContractStablePromotionAllowsOnlyChangelogDiff(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("source drift", func(t *testing.T) {
|
||||
t.Run("commits after beta", 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")
|
||||
mustWriteFile(t, filepath.Join(r.root, "followup.txt"), []byte("merged after beta\n"), 0o644)
|
||||
r.commitAndPush(t, "merge follow-up after beta")
|
||||
|
||||
output, err := runReleaseScript(t, r.root, r.contract,
|
||||
"--repo-root", r.root,
|
||||
@@ -1527,11 +1532,53 @@ func TestReleaseContractStablePromotionAllowsOnlyChangelogDiff(t *testing.T) {
|
||||
"--from-beta", "v1.0.1-beta.1",
|
||||
"--remote", "origin",
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatalf("drifted stable promotion unexpectedly passed:\n%s", output)
|
||||
if err != nil {
|
||||
t.Fatalf("stable promotion with commits after beta error = %v\noutput:\n%s", err, 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)
|
||||
})
|
||||
|
||||
t.Run("beta outside HEAD history", func(t *testing.T) {
|
||||
r := newReleaseTestRepo(t)
|
||||
mustRun(t, r.root, "git", "checkout", "-b", "sidecar")
|
||||
mustWriteFile(t, filepath.Join(r.root, "sidecar.txt"), []byte("never merged\n"), 0o644)
|
||||
mustRun(t, r.root, "git", "add", ".")
|
||||
mustRun(t, r.root, "git", "commit", "-m", "sidecar beta candidate")
|
||||
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", "checkout", "main")
|
||||
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 || !strings.Contains(output, "not an ancestor of HEAD") {
|
||||
t.Fatalf("beta outside HEAD history was promoted: err=%v\noutput:\n%s", err, output)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("older sealed commit after main advanced", 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")
|
||||
sealed := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "HEAD"))
|
||||
mustWriteFile(t, filepath.Join(r.root, "after.txt"), []byte("main advanced\n"), 0o644)
|
||||
r.commitAndPush(t, "advance main after stable candidate")
|
||||
mustRun(t, r.root, "git", "checkout", "--detach", sealed)
|
||||
|
||||
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("older sealed commit in main history was rejected: %v\noutput:\n%s", err, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ func TestWithdrawReleaseScriptDeletesProblemReleaseLastAndUsesPermanentTombstone
|
||||
`github_api --method PATCH`,
|
||||
`npm deprecate "${PACKAGE_NAME}@${VERSION#v}"`,
|
||||
`npm dist-tag add "${PACKAGE_NAME}@${ROLLBACK_VERSION#v}"`,
|
||||
`TARGET_OSS_MODE="$("$SCRIPT_DIR/release-tag-oss-mode.sh" "$VERSION")"`,
|
||||
`printf 'OSS-Mirror: %s\n' "$TARGET_OSS_MODE"`,
|
||||
`oss_mode = fields.get("OSS-Mirror", "enabled")`,
|
||||
`if [ "$OSS_ENABLED" = true ]; then`,
|
||||
`*) err "could not resolve immutable OSS policy for $VERSION" ;;`,
|
||||
`"$OSSUTIL" rm -rf`,
|
||||
`curl -fsS -X DELETE`,
|
||||
`git push "$GITEE_GIT_REMOTE" ":refs/tags/${VERSION}"`,
|
||||
@@ -114,7 +119,7 @@ func TestWithdrawReleaseScriptDeletesProblemReleaseLastAndUsesPermanentTombstone
|
||||
tombstone := strings.LastIndex(script, "\ncreate_tombstone\n")
|
||||
githubMutation := strings.LastIndex(script, "\nupdate_github_release\n")
|
||||
npmMutation := strings.LastIndex(script, "\nupdate_npm_channel\n")
|
||||
ossMutation := strings.LastIndex(script, "\nupdate_oss_channel ")
|
||||
ossMutation := strings.LastIndex(script, `update_oss_channel "$OSS_POINTER_NAME"`)
|
||||
homebrewGate := strings.LastIndex(script, "\nupdate_homebrew\n")
|
||||
githubDelete := strings.LastIndex(script, "\ndelete_github_release_and_tag\n")
|
||||
if tombstone < 0 || githubMutation < 0 || npmMutation < 0 || ossMutation < 0 ||
|
||||
@@ -128,6 +133,122 @@ func TestWithdrawReleaseScriptDeletesProblemReleaseLastAndUsesPermanentTombstone
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseTagOSSModeIsImmutableAndFailClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "release-tag-oss-mode.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(OSS mode script) error = %v", err)
|
||||
}
|
||||
repo := t.TempDir()
|
||||
mustRun(t, repo, "git", "init", "-b", "main")
|
||||
mustRun(t, repo, "git", "config", "user.name", "OSS Mode Test")
|
||||
mustRun(t, repo, "git", "config", "user.email", "oss-mode@example.com")
|
||||
mustWriteFile(t, filepath.Join(repo, "tracked"), []byte("fixture\n"), 0o644)
|
||||
mustRun(t, repo, "git", "add", "tracked")
|
||||
mustRun(t, repo, "git", "commit", "-m", "fixture")
|
||||
|
||||
tag := func(version, message string) {
|
||||
t.Helper()
|
||||
messagePath := filepath.Join(repo, version+".message")
|
||||
mustWriteFile(t, messagePath, []byte(message), 0o644)
|
||||
mustRun(t, repo, "git", "tag", "-a", version, "-F", messagePath)
|
||||
}
|
||||
tag("v1.0.1", "Release v1.0.1\n")
|
||||
tag("v1.0.2", "Release v1.0.2\n\nOSS-Mirror: enabled\n")
|
||||
tag("v1.0.3", "Release v1.0.3\n\nOSS-Mirror: deferred\n")
|
||||
tag("v1.0.4", "Release v1.0.4\n\nOSS-Mirror: invalid\n")
|
||||
tag("v1.0.5", "Release v1.0.5\n\nOSS-Mirror: enabled\nOSS-Mirror: deferred\n")
|
||||
mustRun(t, repo, "git", "tag", "v1.0.6")
|
||||
rawTag := func(version, message string) {
|
||||
t.Helper()
|
||||
commit := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD"))
|
||||
payload := strings.Join([]string{
|
||||
"object " + commit,
|
||||
"type commit",
|
||||
"tag " + version,
|
||||
"tagger OSS Mode Test <oss-mode@example.com> 1700000000 +0000",
|
||||
"",
|
||||
message,
|
||||
}, "\n")
|
||||
cmd := exec.Command("git", "mktag")
|
||||
cmd.Dir = repo
|
||||
cmd.Stdin = strings.NewReader(payload)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git mktag %s error = %v\noutput:\n%s", version, err, output)
|
||||
}
|
||||
mustRun(t, repo, "git", "update-ref", "refs/tags/"+version, strings.TrimSpace(string(output)))
|
||||
}
|
||||
rawTag("v1.0.7", "Release v1.0.7\n\nOSS-Mirror: \n")
|
||||
rawTag("v1.0.8", "Release v1.0.8\r\n\r\nOSS-Mirror: deferred\r\n")
|
||||
|
||||
for _, test := range []struct {
|
||||
version string
|
||||
want string
|
||||
ok bool
|
||||
}{
|
||||
{version: "v1.0.1", want: "enabled", ok: true},
|
||||
{version: "v1.0.2", want: "enabled", ok: true},
|
||||
{version: "v1.0.3", want: "deferred", ok: true},
|
||||
{version: "v1.0.4", want: "invalid OSS-Mirror metadata", ok: false},
|
||||
{version: "v1.0.5", want: "duplicate OSS-Mirror metadata", ok: false},
|
||||
{version: "v1.0.6", want: "must be annotated", ok: false},
|
||||
{version: "v1.0.7", want: "invalid OSS-Mirror metadata", ok: false},
|
||||
{version: "v1.0.8", want: "deferred", ok: true},
|
||||
} {
|
||||
t.Run(test.version, func(t *testing.T) {
|
||||
cmd := exec.Command(scriptPath, test.version)
|
||||
cmd.Dir = repo
|
||||
output, err := cmd.CombinedOutput()
|
||||
if test.ok && err != nil {
|
||||
t.Fatalf("release-tag-oss-mode.sh error = %v\noutput:\n%s", err, output)
|
||||
}
|
||||
if !test.ok && err == nil {
|
||||
t.Fatalf("release-tag-oss-mode.sh unexpectedly accepted %s", test.version)
|
||||
}
|
||||
if !strings.Contains(string(output), test.want) {
|
||||
t.Fatalf("release-tag-oss-mode.sh output missing %q:\n%s", test.want, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawReleaseDefersOSSRequirementUntilImmutablePolicyResolution(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "withdraw-release.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("Abs(withdraw script) error = %v", err)
|
||||
}
|
||||
cmd := exec.Command("bash", scriptPath,
|
||||
"v1.2.3",
|
||||
"critical startup regression",
|
||||
"WITHDRAW v1.2.3",
|
||||
)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GITHUB_TOKEN=test-github-token",
|
||||
"NODE_AUTH_TOKEN=test-npm-token",
|
||||
"GITHUB_REPOSITORY=DingTalk-Real-AI/dingtalk-workspace-cli",
|
||||
"GITHUB_REF_NAME=main",
|
||||
"GITHUB_SHA="+strings.Repeat("a", 40),
|
||||
"GITHUB_RUN_ID=1",
|
||||
"GITHUB_ACTOR=test-operator",
|
||||
"GITHUB_EVENT_DEFAULT_BRANCH=main",
|
||||
"GITHUB_ACTIONS=false",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("withdraw-release.sh unexpectedly ran outside GitHub Actions:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "withdrawal may run only inside GitHub Actions") {
|
||||
t.Fatalf("withdrawal did not reach the protected execution gate before resolving OSS policy:\n%s", output)
|
||||
}
|
||||
if strings.Contains(string(output), "missing required environment variable OSS_") {
|
||||
t.Fatalf("withdrawal required OSS credentials before resolving immutable tag policy:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawReleaseRejectsInvalidInputsBeforeMutation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -180,6 +301,20 @@ func TestWithdrawReleaseRejectsInvalidInputsBeforeMutation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWithdrawReleaseRollsBackConfiguredChannelsAndStopsForHomebrewReview(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
ossDeferred bool
|
||||
}{
|
||||
{name: "legacy tag defaults OSS to enabled"},
|
||||
{name: "deferred OSS survives tombstone retry", ossDeferred: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testWithdrawReleaseRollsBackConfiguredChannelsAndStopsForHomebrewReview(t, test.ossDeferred)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testWithdrawReleaseRollsBackConfiguredChannelsAndStopsForHomebrewReview(t *testing.T, ossDeferred bool) {
|
||||
root := t.TempDir()
|
||||
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
@@ -188,6 +323,7 @@ func TestWithdrawReleaseRollsBackConfiguredChannelsAndStopsForHomebrewReview(t *
|
||||
|
||||
for _, rel := range []string{
|
||||
"scripts/release/withdraw-release.sh",
|
||||
"scripts/release/release-tag-oss-mode.sh",
|
||||
"scripts/release/release-lib.sh",
|
||||
"build/homebrew-release.rb.tmpl",
|
||||
} {
|
||||
@@ -235,6 +371,10 @@ printf 'gitee-sync %s\n' "$VERSION" >> "$CALL_LOG"
|
||||
cp "$DWS_FORMULA_SOURCE" "$MOCK_STATE/homebrew-formula"
|
||||
printf 'homebrew-pr %s %s\n' "$DWS_TAP_PR_TITLE" "$DWS_TAP_PR_BRANCH" >> "$CALL_LOG"
|
||||
exit 0
|
||||
`), 0o755)
|
||||
mustWriteFile(t, filepath.Join(root, "scripts", "release", "unexpected-ossutil"), []byte(`#!/bin/sh
|
||||
printf 'unexpected-oss-call\n' >> "$CALL_LOG"
|
||||
exit 99
|
||||
`), 0o755)
|
||||
|
||||
stateDir := filepath.Join(root, "state")
|
||||
@@ -262,7 +402,11 @@ end
|
||||
`), 0o644)
|
||||
mustRun(t, root, "git", "add", "Formula/dingtalk-workspace-cli.rb")
|
||||
mustRun(t, root, "git", "commit", "-m", "target")
|
||||
mustRun(t, root, "git", "tag", "-a", "v1.0.52", "-m", "Release v1.0.52")
|
||||
tagArgs := []string{"tag", "-a", "v1.0.52", "-m", "Release v1.0.52"}
|
||||
if ossDeferred {
|
||||
tagArgs = append(tagArgs, "-m", "OSS-Mirror: deferred")
|
||||
}
|
||||
mustRun(t, root, "git", tagArgs...)
|
||||
targetCommit := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "HEAD"))
|
||||
targetTagObject := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "refs/tags/v1.0.52"))
|
||||
|
||||
@@ -308,11 +452,6 @@ end
|
||||
"GITHUB_EVENT_DEFAULT_BRANCH=main",
|
||||
"GITHUB_TOKEN=github-token",
|
||||
"NODE_AUTH_TOKEN=npm-token",
|
||||
"OSS_ACCESS_KEY_ID=oss-id",
|
||||
"OSS_ACCESS_KEY_SECRET=oss-secret",
|
||||
"OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com",
|
||||
"OSS_BUCKET=dws-test",
|
||||
"OSSUTIL="+filepath.Join(fakeBin, "ossutil"),
|
||||
"DWS_GITEE_ENABLED=true",
|
||||
"GITEE_TOKEN=gitee-token",
|
||||
"GITEE_USER=gitee-user",
|
||||
@@ -326,6 +465,23 @@ end
|
||||
"DWS_GITEE_SYNC_HELPER="+filepath.Join(root, "scripts", "release", "sync-gitee"),
|
||||
"ORIGIN_GIT="+origin,
|
||||
)
|
||||
if ossDeferred {
|
||||
withdrawEnv = append(withdrawEnv,
|
||||
"OSS_ACCESS_KEY_ID=",
|
||||
"OSS_ACCESS_KEY_SECRET=",
|
||||
"OSS_ENDPOINT=",
|
||||
"OSS_BUCKET=",
|
||||
"OSSUTIL="+filepath.Join(root, "scripts", "release", "unexpected-ossutil"),
|
||||
)
|
||||
} else {
|
||||
withdrawEnv = append(withdrawEnv,
|
||||
"OSS_ACCESS_KEY_ID=oss-id",
|
||||
"OSS_ACCESS_KEY_SECRET=oss-secret",
|
||||
"OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com",
|
||||
"OSS_BUCKET=dws-test",
|
||||
"OSSUTIL="+filepath.Join(fakeBin, "ossutil"),
|
||||
)
|
||||
}
|
||||
cmd.Env = withdrawEnv
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
@@ -338,9 +494,13 @@ end
|
||||
assertFileEquals(t, filepath.Join(stateDir, "latest"), "v1.0.51")
|
||||
assertFileEquals(t, filepath.Join(stateDir, "npm-latest"), "1.0.51")
|
||||
assertFileContains(t, filepath.Join(stateDir, "npm-deprecated"), "WITHDRAWN v1.0.52")
|
||||
assertFileEquals(t, filepath.Join(stateDir, "oss-latest"), "v1.0.51")
|
||||
if _, err := os.Stat(filepath.Join(stateDir, "oss-removed")); err != nil {
|
||||
t.Fatalf("OSS withdrawn prefix was not removed: %v", err)
|
||||
if ossDeferred {
|
||||
assertFileEquals(t, filepath.Join(stateDir, "oss-latest"), "v1.0.52")
|
||||
} else {
|
||||
assertFileEquals(t, filepath.Join(stateDir, "oss-latest"), "v1.0.51")
|
||||
if _, err := os.Stat(filepath.Join(stateDir, "oss-removed")); err != nil {
|
||||
t.Fatalf("OSS withdrawn prefix was not removed: %v", err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(stateDir, "gitee-release-v1.0.52")); !os.IsNotExist(err) {
|
||||
t.Fatalf("Gitee release still exists, stat error = %v", err)
|
||||
@@ -357,6 +517,11 @@ end
|
||||
assertFileContains(t, filepath.Join(stateDir, "tombstone-message"), "Original-Commit: "+targetCommit)
|
||||
assertFileContains(t, filepath.Join(stateDir, "tombstone-message"), "Original-Tag-Object: "+targetTagObject)
|
||||
assertFileContains(t, filepath.Join(stateDir, "tombstone-message"), "Original-Release-ID: 52")
|
||||
ossMode := "enabled"
|
||||
if ossDeferred {
|
||||
ossMode = "deferred"
|
||||
}
|
||||
assertFileContains(t, filepath.Join(stateDir, "tombstone-message"), "OSS-Mirror: "+ossMode)
|
||||
assertFileContains(t, filepath.Join(stateDir, "tombstone-message"), "Reason: critical startup regression")
|
||||
assertFileContains(t, callLog, "homebrew-pr revert: withdraw v1.0.52 and restore v1.0.51")
|
||||
|
||||
@@ -369,20 +534,49 @@ end
|
||||
githubIndex := strings.Index(logText, "github-withdraw")
|
||||
npmIndex := strings.Index(logText, "npm-deprecate")
|
||||
ossIndex := strings.Index(logText, "oss-remove")
|
||||
if tombstoneIndex < 0 || githubIndex < 0 || npmIndex < 0 || ossIndex < 0 {
|
||||
if tombstoneIndex < 0 || githubIndex < 0 || npmIndex < 0 || (!ossDeferred && ossIndex < 0) {
|
||||
t.Fatalf("missing mutation audit entries:\n%s", logText)
|
||||
}
|
||||
homebrewIndex := strings.Index(logText, "homebrew-pr")
|
||||
if !(tombstoneIndex < homebrewIndex && homebrewIndex < githubIndex &&
|
||||
githubIndex < npmIndex && npmIndex < ossIndex) {
|
||||
if !(tombstoneIndex < homebrewIndex && homebrewIndex < githubIndex && githubIndex < npmIndex) ||
|
||||
(!ossDeferred && npmIndex >= ossIndex) {
|
||||
t.Fatalf("tombstone was not durable before channel mutations:\n%s", logText)
|
||||
}
|
||||
deleteReleaseIndex := strings.Index(logText, "github-delete-release")
|
||||
deleteTagIndex := strings.Index(logText, "github-delete-tag")
|
||||
channelsCompleteIndex := npmIndex
|
||||
if !ossDeferred {
|
||||
channelsCompleteIndex = ossIndex
|
||||
}
|
||||
if deleteReleaseIndex < 0 || deleteTagIndex < 0 || homebrewIndex < 0 ||
|
||||
!(homebrewIndex < ossIndex && ossIndex < deleteReleaseIndex && deleteReleaseIndex < deleteTagIndex) {
|
||||
!(homebrewIndex < channelsCompleteIndex && channelsCompleteIndex < deleteReleaseIndex && deleteReleaseIndex < deleteTagIndex) {
|
||||
t.Fatalf("GitHub problem release was not removed before the Homebrew review pause:\n%s", logText)
|
||||
}
|
||||
if ossDeferred {
|
||||
if strings.Contains(logText, "unexpected-oss-call") || ossIndex >= 0 {
|
||||
t.Fatalf("deferred withdrawal invoked OSS tooling:\n%s", logText)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"OSS mirroring is disabled; no OSS rollback is required.",
|
||||
"OSS did not contain v1.0.52 because mirroring was disabled",
|
||||
} {
|
||||
if !strings.Contains(string(output), want) {
|
||||
t.Fatalf("deferred withdrawal output missing %q:\n%s", want, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ossDeferred {
|
||||
tombstonePath := filepath.Join(stateDir, "tombstone-message")
|
||||
tombstoneMessage, err := os.ReadFile(tombstonePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(legacy tombstone) error = %v", err)
|
||||
}
|
||||
legacyMessage := strings.Replace(string(tombstoneMessage), "OSS-Mirror: enabled\n", "", 1)
|
||||
if legacyMessage == string(tombstoneMessage) {
|
||||
t.Fatal("could not convert tombstone fixture to the legacy format")
|
||||
}
|
||||
mustWriteFile(t, tombstonePath, []byte(legacyMessage), 0o644)
|
||||
}
|
||||
|
||||
mergedFormula, err := os.ReadFile(filepath.Join(stateDir, "homebrew-formula"))
|
||||
if err != nil {
|
||||
@@ -420,6 +614,9 @@ end
|
||||
t.Fatalf("withdrawal retry output missing %q:\n%s", want, string(retryOutput))
|
||||
}
|
||||
}
|
||||
if ossDeferred && !strings.Contains(string(retryOutput), "OSS mirroring is disabled; no OSS rollback is required.") {
|
||||
t.Fatalf("deferred tombstone retry did not restore the sealed OSS policy:\n%s", retryOutput)
|
||||
}
|
||||
assertFileEquals(t, filepath.Join(stateDir, "latest"), "v1.0.51")
|
||||
if _, err := os.Stat(filepath.Join(stateDir, "release-v1.0.52.json")); !os.IsNotExist(err) {
|
||||
t.Fatalf("GitHub problem release still exists, stat error = %v", err)
|
||||
|
||||
Reference in New Issue
Block a user