| 📦 Turbo Go 3d7798b k33g 13h ago | 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "os/exec" |
| 6 | "path/filepath" |
| 7 | "runtime" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | // readReleaseScript returns the release builder, so its rules can be asserted |
| 13 | // without running it: running it cross-compiles five binaries, which is not a |
| 14 | // unit test. (Running the tagging script, on the other hand, is done below, |
| 15 | // against a throwaway clone.) |
| 16 | func readReleaseScript(t *testing.T) string { |
| 17 | t.Helper() |
| 18 | |
| 19 | script, err := os.ReadFile("02-build-releases.sh") |
| 20 | if err != nil { |
| 21 | t.Fatalf("cannot read the release script: %v", err) |
| 22 | } |
| 23 | return string(script) |
| 24 | } |
| 25 | |
| 26 | func TestTheReleaseScriptStampsTheBinariesItShips(t *testing.T) { |
| 27 | // Without -ldflags on the cross-compile, every downloaded binary reports |
| 28 | // "devel" while the release page names a version. The host binary would |
| 29 | // still be right, so nothing but this notices. |
| 30 | script := readReleaseScript(t) |
| 31 | |
| 32 | build := commandContaining(t, script, "GOARCH=") |
| 33 | if !strings.Contains(build, "-ldflags") { |
| 34 | t.Errorf("the cross-compile does not stamp a version:\n%s", build) |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | func TestTheReleaseScriptTakesTheStampFromTheMakefile(t *testing.T) { |
| 39 | // Repeating the -X paths in the script is how the host binary and the |
| 40 | // downloads would come to disagree about which package holds the version. |
| 41 | script := readReleaseScript(t) |
| 42 | |
| 43 | if !strings.Contains(script, "make --no-print-directory ldflags") { |
| 44 | t.Error("the script does not read the linker flags from the Makefile") |
| 45 | } |
| 46 | if strings.Contains(script, "version.stamp=") { |
| 47 | t.Error("the script spells out the -X path, which the Makefile already owns") |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | func TestTheReleaseScriptStampsTheTagItIsReleasing(t *testing.T) { |
| 52 | // The release *is* ${TAG}, so that is what the binaries say. Letting the |
| 53 | // Makefile's default stand would stamp `git describe`, which answers a |
| 54 | // different question — where HEAD is — and disagrees the moment anyone |
| 55 | // commits after tagging. |
| 56 | script := readReleaseScript(t) |
| 57 | |
| 58 | flags := commandContaining(t, script, "ldflags") |
| 59 | if !strings.Contains(flags, `VERSION="${TAG}"`) { |
| 60 | t.Errorf("the stamp does not come from TAG:\n%s", flags) |
| 61 | } |
| 62 | if build := commandContaining(t, script, "make build"); !strings.Contains(build, `VERSION="${TAG}"`) { |
| 63 | t.Errorf("the host build carries a different version from the assets:\n%s", build) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestTheReleaseScriptDoesNotParseTheVersionOutOfProse(t *testing.T) { |
| 68 | // `-version` is written for a person and has changed shape once already; |
| 69 | // awk '{print $NF}' on it read a timestamp and failed a release. |
| 70 | script := readReleaseScript(t) |
| 71 | |
| 72 | if strings.Contains(script, "$NF") { |
| 73 | t.Error("the script reads a field out of the -version line, which is prose") |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | func TestTheMakefileHandsOutTheFlagsThatStampABuild(t *testing.T) { |
| 78 | // The contract the release script depends on: `make ldflags` prints flags |
| 79 | // that actually put *the Makefile's own version* into a binary. |
| 80 | // |
| 81 | // It is checked against `make version` rather than against "not devel", |
| 82 | // because a checkout with no tags — a fresh clone, or a repository that has |
| 83 | // never had a release — correctly reports devel, and a test that called |
| 84 | // that a failure would be testing the tags rather than the flags. |
| 85 | version, err := exec.Command("make", "--no-print-directory", "version").Output() |
| 86 | if err != nil { |
| 87 | t.Fatalf("make version: %v", err) |
| 88 | } |
| 89 | // internal/version drops the leading v of a tag, so the comparison has to |
| 90 | // as well: `make version` says v0.2.1 and the binary says 0.2.1. |
| 91 | number := strings.TrimPrefix(strings.Fields(strings.TrimSpace(string(version)))[0], "v") |
| 92 | |
| 93 | flags, err := exec.Command("make", "--no-print-directory", "ldflags").Output() |
| 94 | if err != nil { |
| 95 | t.Fatalf("make ldflags: %v", err) |
| 96 | } |
| 97 | |
| 98 | binary := filepath.Join(t.TempDir(), "turbo-go") |
| 99 | build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") |
| 100 | build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) |
| 101 | if out, err := build.CombinedOutput(); err != nil { |
| 102 | t.Fatalf("building with those flags failed: %v\n%s", err, out) |
| 103 | } |
| 104 | |
| 105 | reported, err := exec.Command(binary, "-version").Output() |
| 106 | if err != nil { |
| 107 | t.Fatalf("the stamped binary does not run: %v", err) |
| 108 | } |
| 109 | if !strings.Contains(string(reported), number) { |
| 110 | t.Errorf("-version printed %q, want it to carry the Makefile's version %q", reported, number) |
| 111 | } |
| 112 | if strings.Contains(string(reported), "unknown") { |
| 113 | t.Errorf("-version printed %q, so nothing reached the linker at all", reported) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestMakeLdflagsTakesTheVersionItIsGiven(t *testing.T) { |
| 118 | // The release script overrides VERSION with the tag it is releasing, and |
| 119 | // everything downstream rests on that override reaching the linker. |
| 120 | flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION=v9.9.9").Output() |
| 121 | if err != nil { |
| 122 | t.Fatalf("make ldflags: %v", err) |
| 123 | } |
| 124 | |
| 125 | binary := filepath.Join(t.TempDir(), "turbo-go") |
| 126 | build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") |
| 127 | build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) |
| 128 | if out, err := build.CombinedOutput(); err != nil { |
| 129 | t.Fatalf("building with those flags failed: %v\n%s", err, out) |
| 130 | } |
| 131 | |
| 132 | reported, err := exec.Command(binary, "-version").Output() |
| 133 | if err != nil { |
| 134 | t.Fatalf("the stamped binary does not run: %v", err) |
| 135 | } |
| 136 | if !strings.Contains(string(reported), "9.9.9") { |
| 137 | t.Errorf("-version printed %q, so VERSION=v9.9.9 never reached the linker", reported) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // commandContaining returns the first shell command of a script holding a |
| 142 | // fragment, with backslash continuations joined: a command's flags are often |
| 143 | // on the line after the one that names it, and a test about the command should |
| 144 | // not depend on where it happens to wrap. |
| 145 | func commandContaining(t *testing.T, script, fragment string) string { |
| 146 | t.Helper() |
| 147 | |
| 148 | joined := strings.ReplaceAll(script, "\\\n", " ") |
| 149 | for _, line := range strings.Split(joined, "\n") { |
| 150 | if strings.Contains(line, fragment) { |
| 151 | return strings.TrimSpace(line) |
| 152 | } |
| 153 | } |
| 154 | t.Fatalf("no command in the script contains %q", fragment) |
| 155 | return "" |
| 156 | } |
| 157 | |
| 158 | // readTagScript returns the tagging script, whose failure modes are what the |
| 159 | // release builder is left to notice when they are not caught here. |
| 160 | func readTagScript(t *testing.T) string { |
| 161 | t.Helper() |
| 162 | |
| 163 | script, err := os.ReadFile("01-release.tag.sh") |
| 164 | if err != nil { |
| 165 | t.Fatalf("cannot read the tagging script: %v", err) |
| 166 | } |
| 167 | return string(script) |
| 168 | } |
| 169 | |
| 170 | func TestTheTagScriptStopsOnTheFirstFailure(t *testing.T) { |
| 171 | // Without this, `git tag` refusing a tag that already existed was skipped |
| 172 | // in silence and the `git push` after it pushed the OLD tag, cutting a |
| 173 | // release from a commit nobody meant. |
| 174 | if !strings.Contains(readTagScript(t), "set -euo pipefail") { |
| 175 | t.Error("the tagging script does not stop on a failing step") |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | func TestTheTagScriptRefusesATagThatAlreadyExists(t *testing.T) { |
| 180 | script := readTagScript(t) |
| 181 | |
| 182 | for _, want := range []string{ |
| 183 | "git rev-parse -q --verify", // taken locally |
| 184 | "git ls-remote --tags origin", // taken on the remote, after a local delete |
| 185 | } { |
| 186 | if !strings.Contains(script, want) { |
| 187 | t.Errorf("the tagging script never checks %q", want) |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | func TestTheTagScriptSurvivesHavingNothingToCommit(t *testing.T) { |
| 193 | // Under `set -e` a plain `git commit` with a clean tree ends the release, |
| 194 | // which is wrong: the work being already committed is the normal case on a |
| 195 | // second run. |
| 196 | script := readTagScript(t) |
| 197 | |
| 198 | if !strings.Contains(script, "git diff --cached --quiet") { |
| 199 | t.Error("the tagging script commits without checking there is anything to commit") |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | func TestTheTagScriptTagsOnlyAfterThePushSucceeded(t *testing.T) { |
| 204 | // A tag left behind pointing at a commit the remote has never seen is the |
| 205 | // state that needs a force push to escape. |
| 206 | script := readTagScript(t) |
| 207 | |
| 208 | push := strings.Index(script, `git push origin "$(git rev-parse`) |
| 209 | tag := strings.Index(script, `git tag -a "${TAG}"`) |
| 210 | if push < 0 || tag < 0 { |
| 211 | t.Fatal("the tagging script no longer pushes and tags") |
| 212 | } |
| 213 | if tag < push { |
| 214 | t.Error("the script tags before pushing, so a rejected push leaves a stray tag") |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // skipInsideARelease stops a test that runs the tagging script from running |
| 219 | // while the tagging script is running it. |
| 220 | // |
| 221 | // The script sets this before `make check`, and `make check` runs this suite. |
| 222 | // Without the guard the two call each other forever — which is not a test-only |
| 223 | // hazard: a real release would recurse in exactly the same way. The Release |
| 224 | // workflow sets it too, for the same reason. |
| 225 | func skipInsideARelease(t *testing.T) { |
| 226 | t.Helper() |
| 227 | if os.Getenv("TURBO_GO_RELEASING") != "" { |
| 228 | t.Skip("running inside a release; not starting another one") |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | func TestTheTagScriptRunsTheSuiteBeforePublishing(t *testing.T) { |
| 233 | // A version people will download, and the proxy will cache, is the wrong |
| 234 | // place to find out the suite was red. |
| 235 | if !strings.Contains(readTagScript(t), "make --no-print-directory check") { |
| 236 | t.Error("the tagging script publishes without running make check") |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | func TestTheTagScriptRefusesAReplaceDirective(t *testing.T) { |
| 241 | // The proxy serves go.mod as written, so `go install …@TAG` on a module |
| 242 | // carrying a replace looks for turbo-core in a directory that does not |
| 243 | // exist on the installer's machine. |
| 244 | if !strings.Contains(readTagScript(t), "replace") { |
| 245 | t.Error("the tagging script does not check go.mod for a replace directive") |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | func TestThisModuleHasNoReplaceDirective(t *testing.T) { |
| 250 | // The check above only helps if it is true today as well. |
| 251 | data, err := os.ReadFile("go.mod") |
| 252 | if err != nil { |
| 253 | t.Fatalf("reading go.mod: %v", err) |
| 254 | } |
| 255 | for _, line := range strings.Split(string(data), "\n") { |
| 256 | if strings.HasPrefix(strings.TrimSpace(line), "replace ") { |
| 257 | t.Errorf("go.mod carries %q; a published module must not", line) |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | func TestTheReleaseToolingNeedsNoPersonalToken(t *testing.T) { |
| 263 | // The Release workflow publishes with the job's own GITHUB_TOKEN, which is |
| 264 | // the only credential Rickub's release API accepts. A script still reading |
| 265 | // a token file is a credential that cannot work and has to be kept |
| 266 | // somewhere all the same — and a 02 or 04 left in the tree is a second |
| 267 | // pipeline somebody will run by mistake. |
| 268 | for _, script := range []string{"01-release.tag.sh", "02-build-releases.sh"} { |
| 269 | data, err := os.ReadFile(script) |
| 270 | if err != nil { |
| 271 | t.Fatalf("reading %s: %v", script, err) |
| 272 | } |
| 273 | for _, secret := range []string{"token.env", "${TOKEN}"} { |
| 274 | if strings.Contains(string(data), secret) { |
| 275 | t.Errorf("%s still reads %s", script, secret) |
| 276 | } |
| 277 | } |
| 278 | } |
| 279 | for _, gone := range []string{"02-release.publish.sh", "04-release.upload-binaries.sh"} { |
| 280 | if _, err := os.Stat(gone); err == nil { |
| 281 | t.Errorf("%s is still there; the workflow publishes and attaches the binaries now", gone) |
| 282 | } |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | func TestTheTagScriptTagsAndPushesForReal(t *testing.T) { |
| 287 | // The whole flow, in a throwaway clone with its own bare remote, so no tag |
| 288 | // is ever created in the real repository. Reading the script is not the |
| 289 | // same as running it: every guard above was added because one of them was |
| 290 | // wrong once. |
| 291 | skipInsideARelease(t) |
| 292 | if _, err := exec.LookPath("git"); err != nil { |
| 293 | t.Skip("git is not available") |
| 294 | } |
| 295 | |
| 296 | remote, clone := throwawayClone(t) |
| 297 | |
| 298 | out, err := runAllowingFailure(t, clone, "./01-release.tag.sh") |
| 299 | if err != nil { |
| 300 | t.Fatalf("the tagging script failed:\n%s", out) |
| 301 | } |
| 302 | if !strings.Contains(out, "published") { |
| 303 | t.Errorf("the script did not report publishing:\n%s", out) |
| 304 | } |
| 305 | |
| 306 | tags, _ := runAllowingFailure(t, remote, "git", "tag") |
| 307 | if !strings.Contains(tags, "v0.0.1-test") { |
| 308 | t.Errorf("the remote has tags %q, want v0.0.1-test", strings.TrimSpace(tags)) |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | func TestTheTagScriptRefusesATagItAlreadyPublished(t *testing.T) { |
| 313 | // Moving a published version is not an option: the proxy caches what it |
| 314 | // fetched, and the release page already carries binaries with that number. |
| 315 | skipInsideARelease(t) |
| 316 | if _, err := exec.LookPath("git"); err != nil { |
| 317 | t.Skip("git is not available") |
| 318 | } |
| 319 | |
| 320 | _, clone := throwawayClone(t) |
| 321 | |
| 322 | if out, err := runAllowingFailure(t, clone, "./01-release.tag.sh"); err != nil { |
| 323 | t.Fatalf("the first release failed:\n%s", out) |
| 324 | } |
| 325 | |
| 326 | out, err := runAllowingFailure(t, clone, "./01-release.tag.sh") |
| 327 | |
| 328 | if err == nil { |
| 329 | t.Fatalf("the script published the same tag twice:\n%s", out) |
| 330 | } |
| 331 | if !strings.Contains(out, "already exists") { |
| 332 | t.Errorf("the refusal does not say the tag is taken:\n%s", out) |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | // throwawayClone sets up a bare remote and a clone of it holding a copy of |
| 337 | // this module and a release.env naming a test version, and returns both paths. |
| 338 | func throwawayClone(t *testing.T) (remote, clone string) { |
| 339 | t.Helper() |
| 340 | |
| 341 | root := t.TempDir() |
| 342 | remote = filepath.Join(root, "remote.git") |
| 343 | clone = filepath.Join(root, "clone") |
| 344 | |
| 345 | runOrFail(t, root, "git", "init", "--bare", "--initial-branch=main", remote) |
| 346 | runOrFail(t, root, "git", "clone", remote, clone) |
| 347 | copyModuleInto(t, clone) |
| 348 | runOrFail(t, clone, "git", "config", "user.email", "test@example.test") |
| 349 | runOrFail(t, clone, "git", "config", "user.name", "Release Test") |
| 350 | writeFile(t, filepath.Join(clone, "release.env"), "TAG=v0.0.1-test\nABOUT=\"a throwaway release\"\n") |
| 351 | return remote, clone |
| 352 | } |
| 353 | |
| 354 | // copyModuleInto copies the module's source into a directory, so the script can |
| 355 | // be run against a real checkout without touching this one. |
| 356 | // |
| 357 | // .git is left out because the target has its own; *.env because a test writes |
| 358 | // its own release.env — copying this checkout's would release whatever version |
| 359 | // happens to be in it; go.work because it would point the copy at a turbo-core |
| 360 | // checkout that is not what a release builds against; and the build outputs |
| 361 | // (bin, release, kits) and the demo project because they are hundreds of |
| 362 | // megabytes the script never reads. |
| 363 | // |
| 364 | // The copying is done here rather than by shelling out to cp, which on a |
| 365 | // network-backed working copy has been seen to write the right number of bytes |
| 366 | // and the wrong ones: every file in the copy came out NUL-filled. |
| 367 | func copyModuleInto(t *testing.T, target string) { |
| 368 | t.Helper() |
| 369 | |
| 370 | entries, err := os.ReadDir(".") |
| 371 | if err != nil { |
| 372 | t.Fatalf("reading the module: %v", err) |
| 373 | } |
| 374 | for _, entry := range entries { |
| 375 | if leftOutOfTheCopy(entry.Name()) { |
| 376 | continue |
| 377 | } |
| 378 | copyTree(t, entry.Name(), filepath.Join(target, entry.Name())) |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | // leftOutOfTheCopy reports whether a top-level entry stays out of a throwaway |
| 383 | // copy of the module. |
| 384 | func leftOutOfTheCopy(name string) bool { |
| 385 | switch name { |
| 386 | case ".git", "bin", "release", "kits", "demo", "go.work", "go.work.sum": |
| 387 | return true |
| 388 | } |
| 389 | return strings.HasSuffix(name, ".env") |
| 390 | } |
| 391 | |
| 392 | // copyTree copies a file or a directory to a new path. |
| 393 | // |
| 394 | // Anything that is neither a regular file nor a directory is skipped: the tool |
| 395 | // directories beside the source hold symlinks into caches that do not exist in |
| 396 | // a temporary copy, and the release scripts have no use for them. |
| 397 | func copyTree(t *testing.T, from, to string) { |
| 398 | t.Helper() |
| 399 | |
| 400 | err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error { |
| 401 | if err != nil { |
| 402 | return err |
| 403 | } |
| 404 | relative, err := filepath.Rel(from, path) |
| 405 | if err != nil { |
| 406 | return err |
| 407 | } |
| 408 | destination := filepath.Join(to, relative) |
| 409 | |
| 410 | if entry.IsDir() { |
| 411 | return os.MkdirAll(destination, 0o755) |
| 412 | } |
| 413 | if !entry.Type().IsRegular() { |
| 414 | return nil |
| 415 | } |
| 416 | info, err := entry.Info() |
| 417 | if err != nil { |
| 418 | return err |
| 419 | } |
| 420 | data, err := os.ReadFile(path) |
| 421 | if err != nil { |
| 422 | return err |
| 423 | } |
| 424 | if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { |
| 425 | return err |
| 426 | } |
| 427 | // The mode carries the execute bit, without which the scripts these |
| 428 | // tests exist to run cannot be run. |
| 429 | return os.WriteFile(destination, data, info.Mode().Perm()) |
| 430 | }) |
| 431 | if err != nil { |
| 432 | t.Fatalf("copying %s: %v", from, err) |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | // runOrFail executes a command in a directory, failing the test if it does not |
| 437 | // succeed. |
| 438 | func runOrFail(t *testing.T, dir string, name string, args ...string) { |
| 439 | t.Helper() |
| 440 | |
| 441 | if out, err := runAllowingFailure(t, dir, name, args...); err != nil { |
| 442 | t.Fatalf("%s %v: %v\n%s", name, args, err, out) |
| 443 | } |
| 444 | } |
| 445 | |
| 446 | // runAllowingFailure executes a command and returns its combined output along |
| 447 | // with whether it succeeded. |
| 448 | // |
| 449 | // GOWORK is switched off for the child: a go.work beside this checkout points |
| 450 | // at a turbo-core working tree, and a release is built against the published |
| 451 | // module, which is what a clean clone would see. |
| 452 | func runAllowingFailure(t *testing.T, dir string, name string, args ...string) (string, error) { |
| 453 | t.Helper() |
| 454 | |
| 455 | command := exec.Command(name, args...) |
| 456 | command.Dir = dir |
| 457 | command.Env = append(os.Environ(), "GOWORK=off") |
| 458 | out, err := command.CombinedOutput() |
| 459 | return string(out), err |
| 460 | } |
| 461 | |
| 462 | // writeFile creates a file, failing the test if it cannot. |
| 463 | func writeFile(t *testing.T, path, contents string) { |
| 464 | t.Helper() |
| 465 | |
| 466 | if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { |
| 467 | t.Fatalf("writing %s: %v", path, err) |
| 468 | } |
| 469 | } |
| 470 | |
| 471 | func TestTheBuildScriptTakesTheTagFromTheCommandLine(t *testing.T) { |
| 472 | // release.env is git-ignored, so the workflow has none: it passes the tag |
| 473 | // it was started by. A script that only reads the file builds nothing in |
| 474 | // CI, or builds whatever version the file last named. |
| 475 | script := readReleaseScript(t) |
| 476 | |
| 477 | if !strings.Contains(script, `TAG="${1:-${TAG:-}}"`) { |
| 478 | t.Error("the build script does not take the tag from its first argument") |
| 479 | } |
| 480 | if !strings.Contains(script, `[ -f release.env ]`) { |
| 481 | t.Error("the build script requires release.env, which CI does not have") |
| 482 | } |
| 483 | } |
| 484 | |
| 485 | func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) { |
| 486 | // The proxy will not serve a tag it cannot read as a version, so a typo |
| 487 | // here builds perfectly and then fails at every `go install`. |
| 488 | // |
| 489 | // The refusal comes before anything is built or written, so the script |
| 490 | // alone is enough: it is run from an empty directory with no release.env. |
| 491 | dir := t.TempDir() |
| 492 | script, err := os.ReadFile("02-build-releases.sh") |
| 493 | if err != nil { |
| 494 | t.Fatalf("reading the build script: %v", err) |
| 495 | } |
| 496 | writeFile(t, filepath.Join(dir, "02-build-releases.sh"), string(script)) |
| 497 | |
| 498 | out, err := runAllowingFailure(t, dir, "bash", "./02-build-releases.sh", "v0.o.0") |
| 499 | |
| 500 | if err == nil { |
| 501 | t.Fatalf("the script accepted a tag that is not a version:\n%s", out) |
| 502 | } |
| 503 | if !strings.Contains(out, "v1.2.3") { |
| 504 | t.Errorf("the refusal does not say what a tag should look like:\n%s", out) |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | func TestTheBuildScriptDoesNotHandOffToAnUploadScript(t *testing.T) { |
| 509 | // 04 attached the binaries to a release page a personal token had created. |
| 510 | // The workflow does both now; a script still pointing at 04 sends the |
| 511 | // reader to run something that is not there. |
| 512 | if strings.Contains(readReleaseScript(t), "04-release") { |
| 513 | t.Error("the build script still hands off to 04-release.upload-binaries.sh") |
| 514 | } |
| 515 | } |
| 516 | |
| 517 | // readWorkflow returns the release workflow's text. |
| 518 | func readWorkflow(t *testing.T) string { |
| 519 | t.Helper() |
| 520 | |
| 521 | data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml")) |
| 522 | if err != nil { |
| 523 | t.Fatalf("reading the release workflow: %v", err) |
| 524 | } |
| 525 | return string(data) |
| 526 | } |
| 527 | |
| 528 | func TestTheWorkflowPublishesOnATagPush(t *testing.T) { |
| 529 | // The tag push is the trigger: ./01-release.tag.sh ends by pushing one, |
| 530 | // and nothing else starts a release. |
| 531 | workflow := readWorkflow(t) |
| 532 | |
| 533 | for _, want := range []string{"push:", "tags:", `- "v*"`} { |
| 534 | if !strings.Contains(workflow, want) { |
| 535 | t.Errorf("the workflow never declares %q", want) |
| 536 | } |
| 537 | } |
| 538 | // A workflow with the default read-only token cannot create a release, and |
| 539 | // fails at its last step after doing all the work. |
| 540 | if !strings.Contains(workflow, "contents: write") { |
| 541 | t.Error("the workflow does not ask for contents: write") |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | func TestTheWorkflowBuildsWithTheSameScriptAPersonRuns(t *testing.T) { |
| 546 | // A CI job that builds its own way is a second pipeline nobody tests, and |
| 547 | // the local one is then only ever exercised by accident. |
| 548 | if !strings.Contains(readWorkflow(t), "./02-build-releases.sh") { |
| 549 | t.Error("the workflow does not build the release with ./02-build-releases.sh") |
| 550 | } |
| 551 | } |
| 552 | |
| 553 | func TestTheWorkflowAttachesWhatWasBuilt(t *testing.T) { |
| 554 | // Publishing a release page with no files attached is a silent half-job: |
| 555 | // the page exists and the downloads are not there. |
| 556 | workflow := readWorkflow(t) |
| 557 | |
| 558 | for _, want := range []string{"turbo-go-*", "SHA256SUMS", "fail_on_unmatched_files: true"} { |
| 559 | if !strings.Contains(workflow, want) { |
| 560 | t.Errorf("the workflow never mentions %q", want) |
| 561 | } |
| 562 | } |
| 563 | } |
| 564 | |
| 565 | func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) { |
| 566 | // A release page is not inside the repository tree, so a relative path |
| 567 | // from it 404s — and a link to the branch would rot as the branch moves. |
| 568 | workflow := readWorkflow(t) |
| 569 | |
| 570 | if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") { |
| 571 | t.Error("the release notes do not link into the repository at the released tag") |
| 572 | } |
| 573 | if !strings.Contains(workflow, "/docs/en/README.md") { |
| 574 | t.Error("the release notes do not link to the documentation") |
| 575 | } |
| 576 | } |
| 577 | |
| 578 | func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) { |
| 579 | // The release API behind Rickub's /gh shim accepts the job's own |
| 580 | // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is |
| 581 | // a credential that cannot work and still has to be kept somewhere. |
| 582 | if strings.Contains(readWorkflow(t), "secrets.") { |
| 583 | t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes") |
| 584 | } |
| 585 | } |
| 586 | |
| 587 | func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) { |
| 588 | // The suite it runs includes tests that run ./01-release.tag.sh against a |
| 589 | // throwaway clone. Locally the script exports this before calling make; |
| 590 | // in CI nothing calls the script, so the job has to set it itself. |
| 591 | if !strings.Contains(readWorkflow(t), "TURBO_GO_RELEASING") { |
| 592 | t.Error("the workflow runs the suite without TURBO_GO_RELEASING set") |
| 593 | } |
| 594 | } |