222 lines
8.2 KiB
Go
222 lines
8.2 KiB
Go
package releasekit_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
|
)
|
|
|
|
const releaseNote = "# Sample App v1.2.3\n\nA source release.\n\nUpgrade notes stay intact.\n"
|
|
|
|
func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) {
|
|
repo := newPublishFixture(t)
|
|
refsBefore := gitText(t, repo.bare, "show-ref", "--heads", "--tags")
|
|
commit := repo.commit
|
|
var validations int
|
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
|
RepositoryDir: repo.work,
|
|
Product: "Sample App",
|
|
Version: "v1.2.3",
|
|
Validate: func(_ context.Context, candidate releasekit.Candidate) error {
|
|
validations++
|
|
if candidate.RepositoryDir != repo.work || candidate.Commit != commit || candidate.Version != "v1.2.3" {
|
|
t.Fatalf("callback candidate = %#v", candidate)
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if validations != 1 {
|
|
t.Fatalf("validation calls = %d, want 1", validations)
|
|
}
|
|
if result.State != releasekit.PublicationPublished || result.Phase != releasekit.PublicationComplete {
|
|
t.Fatalf("publication result = %#v", result)
|
|
}
|
|
if result.CandidateCommit != commit || result.Endpoint != repo.bare || result.TagObject == "" {
|
|
t.Fatalf("publication identity = %#v", result)
|
|
}
|
|
|
|
refsAfter := gitText(t, repo.bare, "show-ref", "--heads", "--tags")
|
|
for _, ref := range strings.Split(refsBefore, "\n") {
|
|
if !strings.Contains(refsAfter, ref) {
|
|
t.Fatalf("preexisting remote ref %q changed or disappeared; refs now:\n%s", ref, refsAfter)
|
|
}
|
|
}
|
|
remoteTag := gitText(t, repo.bare, "rev-parse", "refs/tags/v1.2.3")
|
|
if remoteTag != result.TagObject {
|
|
t.Fatalf("remote tag object = %s, want %s", remoteTag, result.TagObject)
|
|
}
|
|
if peeled := gitText(t, repo.bare, "rev-parse", "refs/tags/v1.2.3^{}"); peeled != commit {
|
|
t.Fatalf("remote tag peels to %s, want %s", peeled, commit)
|
|
}
|
|
if tagType := gitText(t, repo.bare, "cat-file", "-t", "refs/tags/v1.2.3"); tagType != "tag" {
|
|
t.Fatalf("remote tag object type = %q, want tag", tagType)
|
|
}
|
|
tagContents := gitBytes(t, repo.bare, "cat-file", "tag", "refs/tags/v1.2.3")
|
|
separator := strings.Index(tagContents, "\n\n")
|
|
if separator < 0 || tagContents[separator+2:] != releaseNote {
|
|
t.Fatalf("annotated tag message = %q, want %q", tagContents[separator+2:], releaseNote)
|
|
}
|
|
if strings.Contains(tagContents, "-----BEGIN PGP SIGNATURE-----") {
|
|
t.Fatal("publication tag unexpectedly contains a signature")
|
|
}
|
|
if !strings.Contains(refsAfter, "refs/heads/unrelated") || !strings.Contains(refsAfter, "refs/tags/v0.9.0") {
|
|
t.Fatalf("publication changed unrelated refs:\n%s", refsAfter)
|
|
}
|
|
if !strings.Contains(refsAfter, "refs/tags/v1.2.3") || strings.Count(refsAfter, "refs/tags/") != 2 {
|
|
t.Fatalf("publication pushed unexpected tag refs:\n%s", refsAfter)
|
|
}
|
|
}
|
|
|
|
func TestPublishFailureKeepsTheLocalTagAndReportsUnknown(t *testing.T) {
|
|
repo := newPublishFixture(t)
|
|
hook := filepath.Join(repo.work, ".git", "hooks", "pre-push")
|
|
if err := os.WriteFile(hook, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
|
RepositoryDir: repo.work,
|
|
Product: "Sample App",
|
|
Version: "v1.2.3",
|
|
Validate: func(context.Context, releasekit.Candidate) error { return nil },
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Publish() succeeded despite rejected push hook")
|
|
}
|
|
if result.State != releasekit.PublicationUnknown || result.Phase != releasekit.PublicationPush || result.TagObject == "" {
|
|
t.Fatalf("publication result = %#v", result)
|
|
}
|
|
if got := gitText(t, repo.work, "rev-parse", "refs/tags/v1.2.3"); got != result.TagObject {
|
|
t.Fatalf("local tag object = %s, want retained object %s", got, result.TagObject)
|
|
}
|
|
if _, err := exec.Command("git", "--git-dir", repo.bare, "show-ref", "--verify", "--quiet", "refs/tags/v1.2.3").CombinedOutput(); err == nil {
|
|
t.Fatal("rejected push unexpectedly published the tag")
|
|
}
|
|
}
|
|
|
|
func TestPublishRemoteConflictStopsBeforeValidation(t *testing.T) {
|
|
repo := newPublishFixture(t)
|
|
gitRun(t, repo.bare, "update-ref", "refs/heads/main", repo.unrelatedCommit)
|
|
var validations int
|
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
|
RepositoryDir: repo.work,
|
|
Product: "Sample App",
|
|
Version: "v1.2.3",
|
|
Validate: func(context.Context, releasekit.Candidate) error {
|
|
validations++
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Publish() succeeded with a conflicting authoritative main")
|
|
}
|
|
if validations != 0 || result.State != releasekit.PublicationConflict || result.TagObject != "" {
|
|
t.Fatalf("validation calls = %d, result = %#v, error = %v", validations, result, err)
|
|
}
|
|
}
|
|
|
|
func TestPublishRemoteChangeDuringValidationPreventsLocalTag(t *testing.T) {
|
|
repo := newPublishFixture(t)
|
|
result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
|
RepositoryDir: repo.work,
|
|
Product: "Sample App",
|
|
Version: "v1.2.3",
|
|
Validate: func(context.Context, releasekit.Candidate) error {
|
|
gitRun(t, repo.bare, "update-ref", "refs/heads/main", repo.unrelatedCommit)
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Publish() succeeded after authoritative main changed during validation")
|
|
}
|
|
if result.State != releasekit.PublicationConflict || result.Phase != releasekit.PublicationValidation || result.TagObject != "" {
|
|
t.Fatalf("publication result = %#v, error = %v", result, err)
|
|
}
|
|
if _, err := exec.Command("git", "-C", repo.work, "show-ref", "--verify", "--quiet", "refs/tags/v1.2.3").CombinedOutput(); err == nil {
|
|
t.Fatal("tag was created after the remote guard failed")
|
|
}
|
|
}
|
|
|
|
type publishFixture struct {
|
|
work string
|
|
bare string
|
|
commit string
|
|
unrelatedCommit string
|
|
}
|
|
|
|
func newPublishFixture(t *testing.T) publishFixture {
|
|
t.Helper()
|
|
base := t.TempDir()
|
|
work := filepath.Join(base, "work")
|
|
bare := filepath.Join(base, "remote.git")
|
|
gitRun(t, base, "init", "-b", "main", work)
|
|
gitRun(t, work, "config", "user.name", "ReleaseKit Test")
|
|
gitRun(t, work, "config", "user.email", "releasekit@example.invalid")
|
|
writeFile(t, work, "docs/releases/v1.2.3.md", releaseNote)
|
|
writeFile(t, work, "source.txt", "candidate")
|
|
gitRun(t, work, "add", ".")
|
|
gitRun(t, work, "commit", "-m", "Candidate")
|
|
commit := gitText(t, work, "rev-parse", "HEAD")
|
|
gitRun(t, work, "init", "--bare", bare)
|
|
gitRun(t, work, "remote", "add", "origin", bare)
|
|
gitRun(t, work, "push", "origin", "HEAD:refs/heads/main")
|
|
gitRun(t, work, "push", "origin", "HEAD:refs/heads/unrelated")
|
|
gitRun(t, work, "tag", "v0.9.0", commit)
|
|
gitRun(t, work, "push", "origin", "refs/tags/v0.9.0:refs/tags/v0.9.0")
|
|
other := gitText(t, work, "commit-tree", gitText(t, work, "rev-parse", "HEAD^{tree}"), "-p", commit, "-m", "Other remote commit")
|
|
gitRun(t, work, "push", "origin", other+":refs/heads/other")
|
|
gitRun(t, work, "config", "remote.origin.mirror", "true")
|
|
gitRun(t, work, "config", "push.followTags", "true")
|
|
gitRun(t, work, "config", "tag.gpgSign", "true")
|
|
return publishFixture{work: work, bare: bare, commit: commit, unrelatedCommit: other}
|
|
}
|
|
|
|
func gitRun(t *testing.T, dir string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = dir
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
|
|
}
|
|
}
|
|
|
|
func gitText(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = dir
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
|
|
}
|
|
return strings.TrimSuffix(string(output), "\n")
|
|
}
|
|
|
|
func gitBytes(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = dir
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
|
|
}
|
|
return string(output)
|
|
}
|
|
|
|
func writeFile(t *testing.T, root, name, contents string) {
|
|
t.Helper()
|
|
path := filepath.Join(root, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|