Files
dingtalk-workspace-cli/test/scripts/publish_homebrew_formula_test.go
修雨 3e9e76df2c feat: add stable and beta Homebrew channels (#613)
* feat: add stable and beta Homebrew channels

* fix: align beta formula with tap conventions

* fix: use dedicated token for Homebrew PRs

* chore: minimize release token permissions

* docs: record Homebrew token setup

* docs: keep Homebrew automation token long-lived

* fix(verify): assert channel versions and prove homebrew coexistence

The six-channel verifier previously ran `dws version` without comparing
it to the version each channel advertises, so a stale or wrong binary
still reported PASS. It also uninstalled stable before installing beta,
which could not prove the keg-only beta coexists with stable.

- smoke() now takes an expected version and fails the channel on mismatch
- npm/homebrew derive the expected version from the package manager;
  curl/upgrade derive it from the latest GitHub release tag
- homebrew installs keg-only beta while stable stays installed, then
  asserts stable's version, binary SHA and PATH link are unchanged
- cleanup uninstalls both script-installed formulae, still refusing to
  touch a pre-existing user install
- add regression tests for version assertion and coexistence semantics

* style(formula): satisfy brew style for stable and beta formulae

- reword desc so it no longer starts with the formula name
- drop the unnecessary `require "fileutils"` and use the mixed-in cp_r
  instead of the FileUtils. qualifier

* fix(verify): compare channel versions exactly

* fix(homebrew): keep generated formulae style-clean

* fix(homebrew): sync formulae with latest releases
2026-07-15 10:16:26 +08:00

207 lines
7.2 KiB
Go

package scripts_test
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestPublishHomebrewFormulaPushesUpdatedFormula(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "publish-homebrew-formula.sh"))
if err != nil {
t.Fatalf("Abs(publish-homebrew-formula.sh) error = %v", err)
}
root := t.TempDir()
remoteDir := filepath.Join(root, "tap.git")
mustRun(t, root, "git", "init", "--bare", remoteDir)
seedTapRepo(t, remoteDir, "main", "class OldFormula < Formula\nend\n")
sourceFormula := filepath.Join(root, "dingtalk-workspace-cli.rb")
mustWriteFile(t, sourceFormula, []byte("class DingtalkWorkspaceCli < Formula\n desc \"DingTalk Workspace CLI\"\nend\n"), 0o644)
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"DWS_TAP_REPO_URL="+remoteDir,
"DWS_TAP_BRANCH=main",
"DWS_FORMULA_SOURCE="+sourceFormula,
"DWS_GIT_NAME=DWS Bot",
"DWS_GIT_EMAIL=dws@example.com",
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("publish-homebrew-formula.sh error = %v\noutput:\n%s", err, string(output))
}
if !strings.Contains(string(output), "Published Homebrew formula") {
t.Fatalf("publish output missing success message:\n%s", string(output))
}
cloneDir := filepath.Join(root, "check")
mustRun(t, root, "git", "clone", "--branch", "main", remoteDir, cloneDir)
got, err := os.ReadFile(filepath.Join(cloneDir, "Formula", "dingtalk-workspace-cli.rb"))
if err != nil {
t.Fatalf("ReadFile(published formula) error = %v", err)
}
if string(got) != "class DingtalkWorkspaceCli < Formula\n desc \"DingTalk Workspace CLI\"\nend\n" {
t.Fatalf("published formula = %q", string(got))
}
}
func TestPublishHomebrewFormulaSkipsWhenFormulaUnchanged(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "publish-homebrew-formula.sh"))
if err != nil {
t.Fatalf("Abs(publish-homebrew-formula.sh) error = %v", err)
}
root := t.TempDir()
remoteDir := filepath.Join(root, "tap.git")
mustRun(t, root, "git", "init", "--bare", remoteDir)
initialFormula := "class DingtalkWorkspaceCli < Formula\n desc \"DingTalk Workspace CLI\"\nend\n"
seedTapRepo(t, remoteDir, "main", initialFormula)
sourceFormula := filepath.Join(root, "dingtalk-workspace-cli.rb")
mustWriteFile(t, sourceFormula, []byte(initialFormula), 0o644)
beforeHead := strings.TrimSpace(mustOutput(t, root, "git", "ls-remote", remoteDir, "refs/heads/main"))
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"DWS_TAP_REPO_URL="+remoteDir,
"DWS_TAP_BRANCH=main",
"DWS_FORMULA_SOURCE="+sourceFormula,
"DWS_GIT_NAME=DWS Bot",
"DWS_GIT_EMAIL=dws@example.com",
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("publish-homebrew-formula.sh error = %v\noutput:\n%s", err, string(output))
}
if !strings.Contains(string(output), "No formula changes to publish.") {
t.Fatalf("publish output missing no-op message:\n%s", string(output))
}
afterHead := strings.TrimSpace(mustOutput(t, root, "git", "ls-remote", remoteDir, "refs/heads/main"))
if beforeHead != afterHead {
t.Fatalf("remote head changed unexpectedly:\nbefore: %s\nafter: %s", beforeHead, afterHead)
}
}
func TestPublishHomebrewFormulaOpensPRWithoutWritingMain(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "publish-homebrew-formula.sh"))
if err != nil {
t.Fatalf("Abs(publish-homebrew-formula.sh) error = %v", err)
}
root := t.TempDir()
remoteDir := filepath.Join(root, "repo.git")
mustRun(t, root, "git", "init", "--bare", remoteDir)
oldFormula := "class OldFormula < Formula\nend\n"
seedTapRepo(t, remoteDir, "main", oldFormula)
sourceFormula := filepath.Join(root, "dingtalk-workspace-cli.rb")
newFormula := "class DingtalkWorkspaceCli < Formula\n desc \"DingTalk Workspace CLI\"\nend\n"
mustWriteFile(t, sourceFormula, []byte(newFormula), 0o644)
fakeBin := filepath.Join(root, "bin")
ghLog := filepath.Join(root, "gh.log")
mustWriteFile(t, filepath.Join(fakeBin, "gh"), []byte(`#!/bin/sh
printf '%s\n' "$*" >> "$GH_LOG"
if [ "$1 $2" = "pr create" ]; then
printf '%s\n' 'https://github.example/pr/1'
fi
`), 0o755)
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
"GH_LOG="+ghLog,
"DWS_TAP_REPO_URL="+remoteDir,
"DWS_TAP_BRANCH=main",
"DWS_FORMULA_SOURCE="+sourceFormula,
"DWS_TAP_GITHUB_TOKEN=test-token",
"DWS_TAP_PR_REPOSITORY=DingTalk-Real-AI/dingtalk-workspace-cli",
"DWS_TAP_PR_BRANCH=automation/homebrew-v1.2.3",
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("publish-homebrew-formula.sh error = %v\noutput:\n%s", err, string(output))
}
if !strings.Contains(string(output), "Opened Homebrew formula PR: https://github.example/pr/1") {
t.Fatalf("publish output missing PR URL:\n%s", string(output))
}
mainClone := filepath.Join(root, "main-check")
mustRun(t, root, "git", "clone", "--branch", "main", remoteDir, mainClone)
mainFormula, err := os.ReadFile(filepath.Join(mainClone, "Formula", "dingtalk-workspace-cli.rb"))
if err != nil {
t.Fatalf("ReadFile(main formula) error = %v", err)
}
if string(mainFormula) != oldFormula {
t.Fatalf("publisher wrote main directly: %q", string(mainFormula))
}
prClone := filepath.Join(root, "pr-check")
mustRun(t, root, "git", "clone", "--branch", "automation/homebrew-v1.2.3", remoteDir, prClone)
prFormula, err := os.ReadFile(filepath.Join(prClone, "Formula", "dingtalk-workspace-cli.rb"))
if err != nil {
t.Fatalf("ReadFile(PR formula) error = %v", err)
}
if string(prFormula) != newFormula {
t.Fatalf("PR formula = %q, want %q", string(prFormula), newFormula)
}
ghCalls, err := os.ReadFile(ghLog)
if err != nil {
t.Fatalf("ReadFile(gh log) error = %v", err)
}
for _, want := range []string{"pr list", "pr create"} {
if !strings.Contains(string(ghCalls), want) {
t.Errorf("gh calls missing %q:\n%s", want, ghCalls)
}
}
}
func seedTapRepo(t *testing.T, remoteDir, branch, formulaContent string) {
t.Helper()
workDir := t.TempDir()
mustRun(t, t.TempDir(), "git", "clone", remoteDir, workDir)
mustRun(t, workDir, "git", "config", "user.name", "Seed User")
mustRun(t, workDir, "git", "config", "user.email", "seed@example.com")
mustWriteFile(t, filepath.Join(workDir, "Formula", "dingtalk-workspace-cli.rb"), []byte(formulaContent), 0o644)
mustRun(t, workDir, "git", "add", "Formula/dingtalk-workspace-cli.rb")
mustRun(t, workDir, "git", "commit", "-m", "seed")
mustRun(t, workDir, "git", "branch", "-M", branch)
mustRun(t, workDir, "git", "push", "origin", branch)
}
func mustRun(t *testing.T, workdir string, name string, args ...string) {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = workdir
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %v error = %v\noutput:\n%s", name, args, err, string(output))
}
}
func mustOutput(t *testing.T, workdir string, name string, args ...string) string {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = workdir
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %v error = %v\noutput:\n%s", name, args, err, string(output))
}
return string(output)
}