Files

944 lines
35 KiB
Go

package scripts_test
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestWithdrawReleaseWorkflowIsProtectedAndFailClosed(t *testing.T) {
t.Parallel()
workflowPath, err := filepath.Abs(filepath.Join("..", "..", ".github", "workflows", "withdraw-release.yml"))
if err != nil {
t.Fatalf("Abs(withdraw workflow) error = %v", err)
}
content, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", workflowPath, err)
}
workflow := string(content)
for _, want := range []string{
"workflow_dispatch:",
"version:",
"reason:",
"confirmation:",
"group: dws-release-publication",
"cancel-in-progress: false",
"environment: release-withdrawal",
"prevent_self_review !== true",
"deployment_branch_policy?.protected_branches !== true",
"can_admins_bypass !== false",
`const expectedRepository = "DingTalk-Real-AI/dingtalk-workspace-cli"`,
"context.ref !== `refs/heads/${defaultBranch}`",
"branch.data.object.sha !== context.sha",
"contents: write",
"persist-credentials: false",
"NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
"DWS_GITEE_ENABLED:",
"HOMEBREW_PR_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }}",
"./scripts/release/withdraw-release.sh",
"already-installed clients cannot be remotely downgraded.",
"this run remains failed until that PR is independently reviewed",
} {
if !strings.Contains(workflow, want) {
t.Errorf("withdraw workflow missing %q", want)
}
}
for _, forbidden := range []string{
"\n push:",
"\n schedule:",
"npm unpublish",
"cancel-in-progress: true",
} {
if strings.Contains(workflow, forbidden) {
t.Errorf("withdraw workflow contains forbidden trigger/action %q", forbidden)
}
}
}
func TestWithdrawReleaseScriptDeletesProblemReleaseLastAndUsesPermanentTombstone(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)
}
content, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", scriptPath, err)
}
script := string(content)
for _, want := range []string{
`TOMBSTONE="withdrawn/${VERSION}"`,
`github_api --method POST "repos/${OFFICIAL_REPOSITORY}/git/tags"`,
`github_api --method POST "repos/${OFFICIAL_REPOSITORY}/git/refs"`,
"existing tombstone $TOMBSTONE has different immutable withdrawal metadata",
`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}"`,
`)" || return 1`,
`err "could not verify Gitee tag deletion for $VERSION"`,
`disabled_gitee_release_id="$(gitee_release_id "$VERSION")"`,
`DWS_TAP_PR_TITLE="revert: withdraw ${VERSION} and restore ${ROLLBACK_VERSION}"`,
"Homebrew rollback PR requires independent review and merge",
`"repos/${OFFICIAL_REPOSITORY}/releases/${TARGET_RELEASE_ID}"`,
`"repos/${OFFICIAL_REPOSITORY}/git/refs/tags/${VERSION}"`,
`github_expect_404 "repos/${OFFICIAL_REPOSITORY}/releases/tags/${VERSION}"`,
`github_expect_404 "repos/${OFFICIAL_REPOSITORY}/git/ref/tags/${VERSION}"`,
"Already-installed clients cannot be remotely downgraded",
} {
if !strings.Contains(script, want) {
t.Errorf("withdraw script missing %q", want)
}
}
for _, forbidden := range []string{
"npm unpublish",
`"repos/${OFFICIAL_REPOSITORY}/git/refs/tags/${TOMBSTONE}"`,
`--force "refs/tags/${TOMBSTONE}`,
`make_latest`,
} {
if strings.Contains(script, forbidden) {
t.Errorf("withdraw script contains evidence-destroying operation %q", forbidden)
}
}
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, `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 ||
homebrewGate < 0 || githubDelete < 0 {
t.Fatalf("could not locate withdrawal mutation sequence")
}
if !(tombstone < homebrewGate && homebrewGate < githubMutation &&
githubMutation < npmMutation && npmMutation < ossMutation && ossMutation < githubDelete) {
t.Fatalf("unsafe withdrawal order: tombstone=%d github-mark=%d npm=%d oss=%d homebrew=%d github-delete=%d",
tombstone, githubMutation, npmMutation, ossMutation, homebrewGate, githubDelete)
}
}
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()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "withdraw-release.sh"))
if err != nil {
t.Fatalf("Abs(withdraw script) error = %v", err)
}
tests := []struct {
name string
version string
reason string
confirmation string
want string
}{
{
name: "invalid version",
version: "1.2.3",
reason: "critical startup regression",
confirmation: "WITHDRAW 1.2.3",
want: "version must be exactly",
},
{
name: "wrong confirmation",
version: "v1.2.3",
reason: "critical startup regression",
confirmation: "v1.2.3",
want: "confirmation must be exactly: WITHDRAW v1.2.3",
},
{
name: "multiline reason",
version: "v1.2.3",
reason: "critical\nregression",
confirmation: "WITHDRAW v1.2.3",
want: "reason must be a trimmed, printable single line",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
cmd := exec.Command("bash", scriptPath, test.version, test.reason, test.confirmation)
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("withdraw-release.sh unexpectedly accepted invalid input:\n%s", string(output))
}
if !strings.Contains(string(output), test.want) {
t.Fatalf("withdraw-release.sh output missing %q:\n%s", test.want, string(output))
}
})
}
}
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 {
t.Fatalf("Abs(source root) error = %v", err)
}
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",
} {
copyTestFile(t, filepath.Join(sourceRoot, rel), filepath.Join(root, rel), 0o755)
}
mustWriteFile(t, filepath.Join(root, "scripts", "release", "verify-delivery"), []byte(`#!/bin/sh
printf 'delivery %s %s\n' "$1" "$2" >> "$CALL_LOG"
exit 0
`), 0o755)
mustWriteFile(t, filepath.Join(root, "scripts", "release", "download-release-assets"), []byte(`#!/bin/sh
set -eu
version="$1"
dist="$2"
mkdir -p "$dist"
for asset in \
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 \
dws-skills.zip; do
printf '%s %s\n' "$version" "$asset" > "$dist/$asset"
done
{
for asset in \
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 \
dws-skills.zip; do
printf '%064d %s\n' 0 "$asset"
done
} > "$dist/checksums.txt"
printf 'download-assets %s\n' "$version" >> "$CALL_LOG"
`), 0o755)
mustWriteFile(t, filepath.Join(root, "scripts", "release", "verify-release-assets"), []byte(`#!/bin/sh
set -eu
test -f "$DWS_PACKAGE_DIST_DIR/checksums.txt"
printf 'verify-assets %s\n' "$1" >> "$CALL_LOG"
`), 0o755)
mustWriteFile(t, filepath.Join(root, "scripts", "release", "sync-gitee"), []byte(`#!/bin/sh
set -eu
git push "$GITEE_GIT_REMOTE" "refs/tags/$VERSION:refs/tags/$VERSION" >/dev/null
printf '%s\n' "$VERSION" > "$MOCK_STATE/gitee-release-$VERSION"
printf 'gitee-sync %s\n' "$VERSION" >> "$CALL_LOG"
`), 0o755)
mustWriteFile(t, filepath.Join(root, "scripts", "release", "publish-homebrew-formula.sh"), []byte(`#!/bin/sh
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")
fakeBin := filepath.Join(root, "bin")
if err := os.MkdirAll(stateDir, 0o755); err != nil {
t.Fatalf("MkdirAll(state) error = %v", err)
}
callLog := filepath.Join(stateDir, "calls.log")
writeWithdrawMocks(t, fakeBin)
mustWriteFile(t, filepath.Join(root, "Formula", "dingtalk-workspace-cli.rb"), []byte(`class DingtalkWorkspaceCli < Formula
version "1.0.51"
end
`), 0o644)
mustRun(t, root, "git", "init", "-b", "main")
mustRun(t, root, "git", "config", "user.name", "Withdrawal Test")
mustRun(t, root, "git", "config", "user.email", "withdrawal@example.com")
mustRun(t, root, "git", "add", ".")
mustRun(t, root, "git", "commit", "-m", "candidate")
mustRun(t, root, "git", "tag", "-a", "v1.0.51", "-m", "Release v1.0.51")
mustWriteFile(t, filepath.Join(root, "Formula", "dingtalk-workspace-cli.rb"), []byte(`class DingtalkWorkspaceCli < Formula
version "1.0.52"
end
`), 0o644)
mustRun(t, root, "git", "add", "Formula/dingtalk-workspace-cli.rb")
mustRun(t, root, "git", "commit", "-m", "target")
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"))
origin := filepath.Join(root, "origin.git")
gitee := filepath.Join(root, "gitee.git")
mustRun(t, root, "git", "init", "--bare", origin)
mustRun(t, root, "git", "init", "--bare", gitee)
mustRun(t, root, "git", "remote", "add", "origin", origin)
mustRun(t, root, "git", "push", "origin", "main", "v1.0.51", "v1.0.52")
mustRun(t, root, "git", "push", gitee, "v1.0.52")
writeJSONFile(t, filepath.Join(stateDir, "release-v1.0.51.json"), map[string]any{
"id": 51, "tag_name": "v1.0.51", "name": "v1.0.51", "body": "candidate",
"draft": false, "prerelease": false, "immutable": true,
})
writeJSONFile(t, filepath.Join(stateDir, "release-v1.0.52.json"), map[string]any{
"id": 52, "tag_name": "v1.0.52", "name": "v1.0.52", "body": "target",
"draft": false, "prerelease": false, "immutable": true,
})
mustWriteFile(t, filepath.Join(stateDir, "latest"), []byte("v1.0.52\n"), 0o644)
mustWriteFile(t, filepath.Join(stateDir, "npm-latest"), []byte("1.0.52\n"), 0o644)
mustWriteFile(t, filepath.Join(stateDir, "oss-latest"), []byte("v1.0.52\n"), 0o644)
mustWriteFile(t, filepath.Join(stateDir, "gitee-release-v1.0.52"), []byte("752\n"), 0o644)
mustWriteFile(t, filepath.Join(stateDir, "github-tag-v1.0.52"), []byte(targetTagObject+"\n"), 0o644)
scriptPath := filepath.Join(root, "scripts", "release", "withdraw-release.sh")
cmd := exec.Command("bash", scriptPath,
"v1.0.52",
"critical startup regression",
"WITHDRAW v1.0.52",
)
cmd.Dir = root
withdrawEnv := append(os.Environ(),
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
"MOCK_STATE="+stateDir,
"CALL_LOG="+callLog,
"GITHUB_ACTIONS=true",
"GITHUB_REPOSITORY=DingTalk-Real-AI/dingtalk-workspace-cli",
"GITHUB_REF_NAME=main",
"GITHUB_SHA="+targetCommit,
"GITHUB_RUN_ID=12345",
"GITHUB_ACTOR=release-operator",
"GITHUB_EVENT_DEFAULT_BRANCH=main",
"GITHUB_TOKEN=github-token",
"NODE_AUTH_TOKEN=npm-token",
"DWS_GITEE_ENABLED=true",
"GITEE_TOKEN=gitee-token",
"GITEE_USER=gitee-user",
"GITEE_REPO=DingTalk-Real-AI/dingtalk-workspace-cli",
"GITEE_GIT_REMOTE="+gitee,
"GITEE_PUBLIC_GIT_REMOTE="+gitee,
"HOMEBREW_PR_TOKEN=homebrew-token",
"DWS_DELIVERY_VERIFIER="+filepath.Join(root, "scripts", "release", "verify-delivery"),
"DWS_GITHUB_DOWNLOAD_HELPER="+filepath.Join(root, "scripts", "release", "download-release-assets"),
"DWS_ARTIFACT_VERIFY_HELPER="+filepath.Join(root, "scripts", "release", "verify-release-assets"),
"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 {
t.Fatalf("withdrawal must remain failed while Homebrew PR is pending:\n%s", string(output))
}
if !strings.Contains(string(output), "Homebrew rollback PR requires independent review and merge") {
t.Fatalf("withdrawal did not report the Homebrew manual gate:\n%s", string(output))
}
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")
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)
}
if refs := mustOutput(t, root, "git", "ls-remote", gitee, "refs/tags/v1.0.52"); strings.TrimSpace(refs) != "" {
t.Fatalf("Gitee still exposes withdrawn tag:\n%s", refs)
}
if _, err := os.Stat(filepath.Join(stateDir, "release-v1.0.52.json")); !os.IsNotExist(err) {
t.Fatalf("GitHub problem release still exists while Homebrew review is pending, stat error = %v", err)
}
if _, err := os.Stat(filepath.Join(stateDir, "github-tag-v1.0.52")); !os.IsNotExist(err) {
t.Fatalf("GitHub problem tag still exists while Homebrew review is pending, stat error = %v", err)
}
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")
calls, err := os.ReadFile(callLog)
if err != nil {
t.Fatalf("ReadFile(call log) error = %v", err)
}
logText := string(calls)
tombstoneIndex := strings.Index(logText, "tombstone-ref")
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 || (!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) ||
(!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 < 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 {
t.Fatalf("ReadFile(rendered Homebrew rollback) error = %v", err)
}
mustWriteFile(t, filepath.Join(root, "Formula", "dingtalk-workspace-cli.rb"), mergedFormula, 0o644)
mustRun(t, root, "git", "add", "Formula/dingtalk-workspace-cli.rb")
mustRun(t, root, "git", "commit", "-m", "merge Homebrew rollback")
mustRun(t, root, "git", "push", "origin", "main")
retryCommit := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "HEAD"))
retryEnv := replaceTestEnv(withdrawEnv,
"GITHUB_SHA", retryCommit,
"GITHUB_RUN_ID", "12346",
)
retry := exec.Command("bash", scriptPath,
"v1.0.52",
"critical startup regression",
"WITHDRAW v1.0.52",
)
retry.Dir = root
retry.Env = retryEnv
retryOutput, retryErr := retry.CombinedOutput()
if retryErr != nil {
t.Fatalf("withdrawal retry after Homebrew merge error = %v\noutput:\n%s", retryErr, string(retryOutput))
}
for _, want := range []string{
"Resuming withdrawal for v1.0.52 from exact permanent tombstone metadata.",
"Permanent tombstone withdrawn/v1.0.52 already exists",
"GitHub Release v1.0.52 was already absent.",
"GitHub tag v1.0.52 was already absent.",
"Withdrawal completed for all configured distribution channels.",
"Already-installed clients cannot be remotely downgraded",
} {
if !strings.Contains(string(retryOutput), want) {
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)
}
if _, err := os.Stat(filepath.Join(stateDir, "github-tag-v1.0.52")); !os.IsNotExist(err) {
t.Fatalf("GitHub problem tag still exists, stat error = %v", err)
}
if refs := mustOutput(t, root, "git", "ls-remote", origin, "refs/tags/v1.0.52"); strings.TrimSpace(refs) != "" {
t.Fatalf("GitHub origin still exposes withdrawn tag:\n%s", refs)
}
if _, err := os.Stat(filepath.Join(stateDir, "tombstone-ref")); err != nil {
t.Fatalf("permanent withdrawal tombstone is missing: %v", err)
}
assertFileContains(t, callLog, "github-delete-release")
assertFileContains(t, callLog, "github-delete-tag")
}
func copyTestFile(t *testing.T, source, destination string, mode os.FileMode) {
t.Helper()
content, err := os.ReadFile(source)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", source, err)
}
mustWriteFile(t, destination, content, mode)
}
func writeJSONFile(t *testing.T, path string, value any) {
t.Helper()
content, err := json.Marshal(value)
if err != nil {
t.Fatalf("Marshal(%s) error = %v", path, err)
}
mustWriteFile(t, path, append(content, '\n'), 0o644)
}
func assertFileEquals(t *testing.T, path, want string) {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", path, err)
}
if got := strings.TrimSpace(string(content)); got != want {
t.Fatalf("%s = %q, want %q", path, got, want)
}
}
func assertFileContains(t *testing.T, path, want string) {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", path, err)
}
if !strings.Contains(string(content), want) {
t.Fatalf("%s missing %q:\n%s", path, want, string(content))
}
}
func replaceTestEnv(env []string, replacements ...string) []string {
result := append([]string{}, env...)
for index := 0; index < len(replacements); index += 2 {
key := replacements[index]
value := replacements[index+1]
prefix := key + "="
replaced := false
for envIndex, entry := range result {
if strings.HasPrefix(entry, prefix) {
result[envIndex] = prefix + value
replaced = true
}
}
if !replaced {
result = append(result, prefix+value)
}
}
return result
}
func writeWithdrawMocks(t *testing.T, fakeBin string) {
t.Helper()
mustWriteFile(t, filepath.Join(fakeBin, "gh"), []byte(`#!/usr/bin/env python3
import json
import os
import pathlib
import subprocess
import sys
state = pathlib.Path(os.environ["MOCK_STATE"])
log = pathlib.Path(os.environ["CALL_LOG"])
args = sys.argv[1:]
def record(value):
with log.open("a", encoding="utf-8") as handle:
handle.write(value + "\n")
def value_after(flag, default=""):
try:
return args[args.index(flag) + 1]
except (ValueError, IndexError):
return default
def not_found():
print("gh: Not Found (HTTP 404)", file=sys.stderr)
raise SystemExit(1)
if args[:2] == ["release", "download"]:
output = pathlib.Path(value_after("--dir"))
output.mkdir(parents=True, exist_ok=True)
assets = [
"dws-darwin-amd64.tar.gz",
"dws-darwin-arm64.tar.gz",
"dws-linux-amd64.tar.gz",
"dws-linux-arm64.tar.gz",
"dws-skills.zip",
]
(output / "checksums.txt").write_text(
"".join(("a" * 64) + " " + asset + "\n" for asset in assets),
encoding="utf-8",
)
record("github-download-checksums")
raise SystemExit(0)
if not args or args[0] != "api":
raise SystemExit("unsupported gh invocation: " + " ".join(args))
method = value_after("--method", "GET")
skip = {"-H", "--method", "-f", "-F", "--input", "--jq"}
endpoint = ""
i = 1
while i < len(args):
if args[i] in skip:
i += 2
continue
if args[i].startswith("repos/"):
endpoint = args[i]
break
i += 1
jq = value_after("--jq")
if endpoint.endswith("/git/ref/heads/main"):
payload = {"object": {"sha": os.environ["GITHUB_SHA"]}}
elif "/releases/tags/" in endpoint:
version = endpoint.rsplit("/", 1)[1]
path = state / f"release-{version}.json"
if not path.exists():
not_found()
payload = json.loads(path.read_text(encoding="utf-8"))
elif endpoint.endswith("/releases/latest"):
payload = {"tag_name": (state / "latest").read_text(encoding="utf-8").strip()}
elif "/git/ref/tags/withdrawn/" in endpoint:
ref_path = state / "tombstone-ref"
if not ref_path.exists():
not_found()
payload = {"object": {"sha": ref_path.read_text(encoding="utf-8").strip()}}
elif endpoint.endswith("/git/ref/tags/v1.0.52") and method == "GET":
tag_path = state / "github-tag-v1.0.52"
if not tag_path.exists():
not_found()
payload = {"object": {"sha": tag_path.read_text(encoding="utf-8").strip()}}
elif "/git/tags/" in endpoint and method == "GET":
payload = {
"tag": "withdrawn/v1.0.52",
"message": (state / "tombstone-message").read_text(encoding="utf-8"),
"object": {
"type": "commit",
"sha": (state / "tombstone-target").read_text(encoding="utf-8").strip(),
},
}
elif endpoint.endswith("/git/tags") and method == "POST":
fields = {}
for index, arg in enumerate(args):
if arg == "-f":
key, value = args[index + 1].split("=", 1)
fields[key] = value
(state / "tombstone-message").write_text(fields["message"], encoding="utf-8")
(state / "tombstone-target").write_text(fields["object"] + "\n", encoding="utf-8")
payload = {"sha": "a" * 40}
record("tombstone-object")
elif endpoint.endswith("/git/refs") and method == "POST":
(state / "tombstone-ref").write_text("a" * 40 + "\n", encoding="utf-8")
payload = {"ref": "refs/tags/withdrawn/v1.0.52", "object": {"sha": "a" * 40}}
record("tombstone-ref")
elif endpoint.endswith("/releases/52") and method == "DELETE":
release_path = state / "release-v1.0.52.json"
if not release_path.exists():
not_found()
release_path.unlink()
(state / "latest").write_text("v1.0.51\n", encoding="utf-8")
payload = {}
record("github-delete-release")
elif endpoint.endswith("/git/refs/tags/v1.0.52") and method == "DELETE":
tag_path = state / "github-tag-v1.0.52"
if not tag_path.exists():
not_found()
tag_path.unlink()
subprocess.run(
["git", f"--git-dir={os.environ['ORIGIN_GIT']}", "update-ref", "-d", "refs/tags/v1.0.52"],
check=True,
)
payload = {}
record("github-delete-tag")
elif "/releases/" in endpoint and method == "PATCH":
release_id = endpoint.rsplit("/", 1)[1]
if release_id == "52":
path = state / "release-v1.0.52.json"
data = json.loads(path.read_text(encoding="utf-8"))
input_path = value_after("--input")
patch = json.loads(pathlib.Path(input_path).read_text(encoding="utf-8"))
data.update({"name": patch["name"], "body": patch["body"]})
path.write_text(json.dumps(data), encoding="utf-8")
record("github-withdraw")
payload = {}
else:
raise SystemExit("unsupported gh api endpoint: " + endpoint + " " + method)
if jq == ".object.sha":
print(payload["object"]["sha"])
elif jq == ".tag_name":
print(payload["tag_name"])
elif jq == ".sha":
print(payload["sha"])
else:
print(json.dumps(payload))
`), 0o755)
mustWriteFile(t, filepath.Join(fakeBin, "npm"), []byte(`#!/usr/bin/env python3
import os
import pathlib
import sys
state = pathlib.Path(os.environ["MOCK_STATE"])
log = pathlib.Path(os.environ["CALL_LOG"])
args = sys.argv[1:]
def record(value):
with log.open("a", encoding="utf-8") as handle:
handle.write(value + "\n")
if args[0] == "view":
spec = args[1]
field = args[2]
if field == "version":
print(spec.rsplit("@", 1)[1])
elif field == "deprecated":
path = state / "npm-deprecated"
if path.exists() and spec.endswith("@1.0.52"):
print(path.read_text(encoding="utf-8").strip())
elif field == "dist-tags.latest":
print((state / "npm-latest").read_text(encoding="utf-8").strip())
else:
raise SystemExit("unsupported npm view field: " + field)
elif args[0] == "deprecate":
(state / "npm-deprecated").write_text(args[2] + "\n", encoding="utf-8")
record("npm-deprecate")
elif args[:2] == ["dist-tag", "add"]:
version = args[2].rsplit("@", 1)[1]
(state / "npm-latest").write_text(version + "\n", encoding="utf-8")
record("npm-pointer")
else:
raise SystemExit("unsupported npm invocation: " + " ".join(args))
`), 0o755)
mustWriteFile(t, filepath.Join(fakeBin, "ossutil"), []byte(`#!/usr/bin/env python3
import os
import pathlib
import sys
state = pathlib.Path(os.environ["MOCK_STATE"])
log = pathlib.Path(os.environ["CALL_LOG"])
args = sys.argv[1:]
if args[0] == "cp":
source, target = args[-2:]
if source.startswith("oss://"):
if source.endswith("/latest.txt"):
stored = state / "oss-latest"
else:
stored = state / ("oss-object-" + source.rsplit("/", 1)[1])
pathlib.Path(target).write_bytes(stored.read_bytes())
else:
if target.endswith("/latest.txt"):
stored = state / "oss-latest"
else:
stored = state / ("oss-object-" + target.rsplit("/", 1)[1])
stored.write_bytes(pathlib.Path(source).read_bytes())
elif args[0] == "rm":
(state / "oss-removed").write_text("yes\n", encoding="utf-8")
with log.open("a", encoding="utf-8") as handle:
handle.write("oss-remove\n")
elif args[0] == "ls":
raise SystemExit(0)
else:
raise SystemExit("unsupported ossutil invocation: " + " ".join(args))
`), 0o755)
mustWriteFile(t, filepath.Join(fakeBin, "curl"), []byte(`#!/usr/bin/env python3
import json
import os
import pathlib
import sys
state = pathlib.Path(os.environ["MOCK_STATE"])
log = pathlib.Path(os.environ["CALL_LOG"])
args = sys.argv[1:]
url = args[-1]
version = "v1.0.51" if "v1.0.51" in url else "v1.0.52"
release = state / ("gitee-release-" + version)
if "-X" in args and args[args.index("-X") + 1] == "DELETE":
(state / "gitee-release-v1.0.52").unlink(missing_ok=True)
with log.open("a", encoding="utf-8") as handle:
handle.write("gitee-delete-release\n")
elif "/releases/tags/" in url:
output = pathlib.Path(args[args.index("-o") + 1])
if release.exists():
release_id = 751 if version == "v1.0.51" else 752
output.write_text(json.dumps({"id": release_id}), encoding="utf-8")
print("200", end="")
else:
output.write_text('{"message":"Not Found"}', encoding="utf-8")
print("404", end="")
else:
raise SystemExit(22)
`), 0o755)
}