2026-10-05 18:24:37 +00:00
package releasekit_test
import (
"context"
2026-10-05 18:53:44 +00:00
"errors"
"fmt"
2026-10-05 18:24:37 +00:00
"os"
"os/exec"
"path/filepath"
2026-10-07 22:10:34 +00:00
"runtime"
2026-10-05 18:24:37 +00:00
"strings"
"testing"
2026-10-05 18:53:44 +00:00
"time"
2026-10-05 18:24:37 +00:00
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
2026-10-06 16:04:35 +00:00
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
2026-10-05 18:24:37 +00:00
)
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 )
2026-10-05 18:53:44 +00:00
wrapper := installGitWrapper ( t , repo , "pass" )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
2026-10-06 17:15:46 +00:00
suiteRuns := filepath . Join ( t . TempDir ( ) , "suite-runs" )
t . Setenv ( "RELEASEKIT_FIXTURE_SUITE_RUNS" , suiteRuns )
2026-10-05 18:24:37 +00:00
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" ,
2026-10-06 17:15:46 +00:00
AdditionalChecks : func ( _ context . Context , candidate releasekit . Candidate ) error {
2026-10-05 18:24:37 +00:00
validations ++
2026-10-06 17:15:46 +00:00
runs , readErr := os . ReadFile ( suiteRuns )
if readErr != nil || string ( runs ) != "ran\n" {
return fmt . Errorf ( "standard suite runs before additional checks: contents=%q error=%v" , runs , readErr )
}
2026-10-10 02:15:13 +00:00
if candidate . RepositoryDir != repo . work || candidate . ModuleDir != repo . work || candidate . ModulePath != "example.com/releasekit-publish-fixture" || candidate . Tag != "v1.2.3" || candidate . Commit != commit || candidate . Version != "v1.2.3" {
2026-10-05 18:24:37 +00:00
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 )
}
2026-10-06 17:15:46 +00:00
if runs , err := os . ReadFile ( suiteRuns ) ; err != nil || string ( runs ) != "ran\n" {
t . Fatalf ( "standard suite runs = %q, error = %v; want one run" , runs , err )
}
2026-10-05 18:53:44 +00:00
if queries := countLines ( t , wrapper . queryLog ) ; queries != 3 {
t . Fatalf ( "authoritative tag query count = %d, want 3 guards and no success read-back" , queries )
}
2026-10-05 18:24:37 +00:00
if result . State != releasekit . PublicationPublished || result . Phase != releasekit . PublicationComplete {
t . Fatalf ( "publication result = %#v" , result )
}
2026-10-10 02:15:13 +00:00
if result . RepositoryDir != repo . work || result . ModuleDir != repo . work || result . ModulePath != "example.com/releasekit-publish-fixture" || result . Tag != "v1.2.3" || result . CandidateCommit != commit || result . Endpoint != repo . bare || result . TagObject == "" {
2026-10-05 18:24:37 +00:00
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 )
}
}
2026-10-10 02:18:02 +00:00
func TestPublishUsesSeparateFullTagsForModulesAtOneCommit ( t * testing . T ) {
repo := newPublishFixture ( t )
firstDir , secondDir := "integrations/first" , "integrations/second"
addNestedReleaseModule ( t , repo . work , firstDir , "example.com/first" , "v1.2.3" )
addNestedReleaseModule ( t , repo . work , secondDir , "example.com/second" , "v1.2.3" )
gitRun ( t , repo . work , "add" , firstDir , secondDir )
gitRun ( t , repo . work , "commit" , "-m" , "add two release modules" )
gitRun ( t , repo . work , "push" , "origin" )
commit := gitText ( t , repo . work , "rev-parse" , "HEAD" )
for _ , moduleDir := range [ ] string { firstDir , secondDir } {
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
ModuleDir : moduleDir ,
Product : "Sample App" ,
Version : "v1.2.3" ,
} )
if err != nil {
t . Fatalf ( "Publish(%q): %v" , moduleDir , err )
}
wantTag := moduleDir + "/v1.2.3"
if result . CandidateCommit != commit || result . Tag != wantTag || result . Version != "v1.2.3" {
t . Fatalf ( "publication for %q = %#v" , moduleDir , result )
}
if got := gitText ( t , repo . bare , "rev-parse" , "refs/tags/" + wantTag ) ; got != result . TagObject {
t . Fatalf ( "remote tag %s object = %s, want %s" , wantTag , got , result . TagObject )
}
}
refs := gitText ( t , repo . bare , "for-each-ref" , "--format=%(refname)" , "refs/tags" )
if strings . Contains ( refs , "refs/tags/v1.2.3\n" ) || ! strings . Contains ( refs , "refs/tags/" + firstDir + "/v1.2.3" ) || ! strings . Contains ( refs , "refs/tags/" + secondDir + "/v1.2.3" ) {
t . Fatalf ( "published tag refs = %q" , refs )
}
firstTag := firstDir + "/v1.2.3"
gitRun ( t , repo . work , "tag" , "-d" , firstTag )
conflict , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
ModuleDir : firstDir ,
Product : "Sample App" ,
Version : "v1.2.3" ,
} )
if ! errors . Is ( err , git . ErrRemoteTagConflict ) || conflict . State != releasekit . PublicationConflict || conflict . Tag != firstTag {
t . Fatalf ( "reuse of remote tag result=%#v error=%v" , conflict , err )
}
}
func TestPublishNestedRejectedPushReportsFullTag ( t * testing . T ) {
repo := newPublishFixture ( t )
moduleDir := "integrations/ckconfig"
addNestedReleaseModule ( t , repo . work , moduleDir , "example.com/ckconfig" , "v1.2.3" )
gitRun ( t , repo . work , "add" , moduleDir )
gitRun ( t , repo . work , "commit" , "-m" , "add nested module" )
gitRun ( t , repo . work , "push" , "origin" )
hook := filepath . Join ( repo . bare , "hooks" , "pre-receive" )
if err := os . WriteFile ( hook , [ ] byte ( "#!/bin/sh\ncat >/dev/null\nexit 1\n" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
wantTag := moduleDir + "/v1.2.3"
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
ModuleDir : moduleDir ,
Product : "Sample App" ,
Version : "v1.2.3" ,
} )
if err == nil || result . State != releasekit . PublicationNotPublished || result . Tag != wantTag {
t . Fatalf ( "nested rejected push result=%#v error=%v" , result , err )
}
if ! strings . Contains ( err . Error ( ) , wantTag ) || ! strings . Contains ( err . Error ( ) , repo . bare ) || ! strings . Contains ( err . Error ( ) , "authoritative tag is absent" ) {
t . Fatalf ( "nested rejected-push error = %v" , err )
}
if _ , err := exec . Command ( "git" , "--git-dir" , repo . bare , "show-ref" , "--verify" , "--quiet" , "refs/tags/" + wantTag ) . CombinedOutput ( ) ; err == nil {
t . Fatal ( "rejected nested tag unexpectedly appeared on the remote" )
}
}
func TestPublishNestedTagKeepsUnknownReconciliationEvidence ( t * testing . T ) {
repo := newPublishFixture ( t )
moduleDir := "integrations/ckconfig"
addNestedReleaseModule ( t , repo . work , moduleDir , "example.com/ckconfig" , "v1.2.3" )
gitRun ( t , repo . work , "add" , moduleDir )
gitRun ( t , repo . work , "commit" , "-m" , "add nested module" )
gitRun ( t , repo . work , "push" , "origin" )
hook := filepath . Join ( repo . bare , "hooks" , "pre-receive" )
if err := os . WriteFile ( hook , [ ] byte ( "#!/bin/sh\ncat >/dev/null\nexit 1\n" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
wantTag := moduleDir + "/v1.2.3"
wrapper := installTagQueryFailureWrapper ( t , wantTag )
t . Setenv ( "PATH" , wrapper + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
ModuleDir : moduleDir ,
Product : "Sample App" ,
Version : "v1.2.3" ,
} )
if err == nil || result . State != releasekit . PublicationUnknown || result . Tag != wantTag || result . TagObject == "" {
t . Fatalf ( "nested unknown result=%#v error=%v" , result , err )
}
if ! strings . Contains ( err . Error ( ) , wantTag ) || ! strings . Contains ( err . Error ( ) , repo . bare ) || ! strings . Contains ( err . Error ( ) , "reconciliation unavailable" ) || ! strings . Contains ( err . Error ( ) , "failed to push some refs" ) {
t . Fatalf ( "nested reconciliation error = %v" , err )
}
}
2026-10-07 22:31:58 +00:00
func TestPublishRejectsUnexpectedPushURLBeforeValidation ( t * testing . T ) {
repo := newPublishFixture ( t )
suiteRuns := filepath . Join ( t . TempDir ( ) , "suite-runs" )
t . Setenv ( "RELEASEKIT_FIXTURE_SUITE_RUNS" , suiteRuns )
expected := repo . bare + string ( os . PathSeparator )
var callbacks int
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
ExpectedPushURL : expected ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
callbacks ++
return nil
} ,
} )
if ! errors . Is ( err , releasekit . ErrUnexpectedPushURL ) {
t . Fatalf ( "Publish() error = %v, want ErrUnexpectedPushURL" , err )
}
if ! strings . Contains ( err . Error ( ) , fmt . Sprintf ( "expected %q" , expected ) ) || ! strings . Contains ( err . Error ( ) , fmt . Sprintf ( "resolved %q" , repo . bare ) ) {
t . Fatalf ( "Publish() error = %v, want expected and resolved URLs" , err )
}
if result . State != releasekit . PublicationNotAttempted || result . Phase != releasekit . PublicationPreflight || result . Endpoint != repo . bare || result . TagObject != "" {
t . Fatalf ( "publication result = %#v, want preflight failure at resolved endpoint" , result )
}
if callbacks != 0 {
t . Fatalf ( "additional checks ran %d times, want none" , callbacks )
}
if _ , statErr := os . Stat ( suiteRuns ) ; ! errors . Is ( statErr , os . ErrNotExist ) {
t . Fatalf ( "standard-suite marker stat error = %v, want no suite run" , statErr )
}
if got := gitText ( t , repo . work , "tag" , "--list" , "v1.2.3" ) ; got != "" {
t . Fatalf ( "local tag exists after URL mismatch: %q" , got )
}
}
func TestPublishComparesExpectedURLWithResolvedInsteadOfDestination ( t * testing . T ) {
repo := newPublishFixture ( t )
alias := "releasekit-expected-push:"
gitRun ( t , repo . work , "config" , "url." + repo . bare + ".insteadOf" , alias )
gitRun ( t , repo . work , "remote" , "set-url" , "origin" , alias )
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
ExpectedPushURL : repo . bare ,
} )
if err != nil {
t . Fatalf ( "Publish() with the resolved insteadOf destination failed: %v" , err )
}
if result . Endpoint != repo . bare || result . State != releasekit . PublicationPublished {
t . Fatalf ( "publication result = %#v, want resolved push URL and successful publication" , result )
}
}
func TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer ( t * testing . T ) {
repo := newPublishFixture ( t )
fetchURL := filepath . Join ( t . TempDir ( ) , "fetch-only.git" )
gitRun ( t , repo . work , "remote" , "set-url" , "origin" , fetchURL )
gitRun ( t , repo . work , "config" , "remote.origin.pushurl" , repo . bare )
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
ExpectedPushURL : fetchURL ,
} )
if ! errors . Is ( err , releasekit . ErrUnexpectedPushURL ) {
t . Fatalf ( "Publish() error = %v, want ErrUnexpectedPushURL" , err )
}
if result . Endpoint != repo . bare || ! strings . Contains ( err . Error ( ) , fmt . Sprintf ( "resolved %q" , repo . bare ) ) {
t . Fatalf ( "result = %#v, error = %v; want the effective push URL %q" , result , err , repo . bare )
}
}
func TestCheckIgnoresExpectedPushURL ( t * testing . T ) {
repo := newPublishFixture ( t )
var callbacks int
_ , err := releasekit . Check ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
ExpectedPushURL : "unrelated destination" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
callbacks ++
return nil
} ,
} )
if err != nil {
t . Fatalf ( "Check() considered publication URL expectation: %v" , err )
}
if callbacks != 1 {
t . Fatalf ( "additional checks ran %d times, want one" , callbacks )
}
}
2026-10-07 22:10:34 +00:00
func TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks ( t * testing . T ) {
requireRacePrerequisites ( t )
repo := newPublishFixture ( t )
raceMarker := filepath . Join ( t . TempDir ( ) , "race-enabled" )
t . Setenv ( "RELEASEKIT_RACE_MARKER" , raceMarker )
checks := 0
_ , err := releasekit . Check ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
Validation : releasekit . ValidationSpec {
Go : releasekit . GoValidationSpec { Race : true } ,
} ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
checks ++
return nil
} ,
} )
if err != nil {
t . Fatalf ( "Check() error = %v" , err )
}
if checks != 1 {
t . Fatalf ( "additional checks ran %d times, want once" , checks )
}
if _ , err := os . Stat ( raceMarker ) ; err != nil {
t . Fatalf ( "race-enabled fixture did not run: %v" , err )
}
}
2026-10-06 17:15:46 +00:00
func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation ( t * testing . T ) {
repo := newPublishFixture ( t )
t . Setenv ( "RELEASEKIT_FAIL_FIXTURE_SUITE" , "1" )
var checks int
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
checks ++
return nil
} ,
} )
if err == nil || ! strings . Contains ( err . Error ( ) , "fixture standard suite failure" ) {
t . Fatalf ( "Publish() error = %v, want fixture standard suite failure" , err )
}
2026-10-07 18:34:34 +00:00
var validationError * releasekit . ValidationError
if ! errors . As ( err , & validationError ) || validationError . Check != "go-test" {
t . Fatalf ( "Publish() error = %v, want a public go-test validation error" , err )
}
var exitErr * exec . ExitError
if ! errors . As ( err , & exitErr ) {
t . Fatalf ( "Publish() error = %v, want the Go process error preserved" , err )
}
if ! errors . Is ( err , exitErr ) {
t . Fatal ( "Publish() error does not preserve the Go process error identity" )
}
2026-10-06 17:15:46 +00:00
if checks != 0 || result . TagObject != "" || result . State != releasekit . PublicationNotAttempted {
t . Fatalf ( "failed standard suite result=%#v additional checks=%d" , result , checks )
}
for _ , command := range [ ] [ ] string {
{ "git" , "-C" , repo . work , "show-ref" , "--verify" , "--quiet" , "refs/tags/v1.2.3" } ,
{ "git" , "--git-dir" , repo . bare , "show-ref" , "--verify" , "--quiet" , "refs/tags/v1.2.3" } ,
} {
if _ , err := exec . Command ( command [ 0 ] , command [ 1 : ] ... ) . CombinedOutput ( ) ; err == nil {
t . Fatalf ( "failed standard suite created release tag with %v" , command )
}
}
}
2026-10-07 22:10:34 +00:00
func requireRacePrerequisites ( t * testing . T ) {
t . Helper ( )
supported := runtime . GOOS == "darwin" && ( runtime . GOARCH == "amd64" || runtime . GOARCH == "arm64" ) ||
( runtime . GOOS == "freebsd" || runtime . GOOS == "linux" ) && ( runtime . GOARCH == "amd64" || runtime . GOARCH == "arm64" || runtime . GOARCH == "ppc64le" ) ||
runtime . GOOS == "windows" && runtime . GOARCH == "amd64"
if ! supported {
t . Skipf ( "Go race detector is not supported on %s/%s" , runtime . GOOS , runtime . GOARCH )
}
cgo , err := exec . Command ( "go" , "env" , "CGO_ENABLED" ) . Output ( )
if err != nil || strings . TrimSpace ( string ( cgo ) ) != "1" {
t . Skip ( "Go race detector requires CGO_ENABLED=1" )
}
cc , err := exec . Command ( "go" , "env" , "CC" ) . Output ( )
if err != nil || len ( strings . Fields ( string ( cc ) ) ) == 0 {
t . Skip ( "Go race detector requires a C compiler" )
}
if _ , err := exec . LookPath ( strings . Fields ( string ( cc ) ) [ 0 ] ) ; err != nil {
t . Skip ( "Go race detector C compiler is unavailable" )
}
}
2026-10-07 18:34:34 +00:00
func TestCheckReturnsPublicValidationError ( t * testing . T ) {
repo := newPublishFixture ( t )
t . Setenv ( "RELEASEKIT_FAIL_FIXTURE_SUITE" , "1" )
2026-10-10 02:15:13 +00:00
captured , err := releasekit . Check ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-07 18:34:34 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
} )
var validationError * releasekit . ValidationError
if ! errors . As ( err , & validationError ) || validationError . Check != "go-test" {
t . Fatalf ( "Check() error = %v, want a public go-test validation error" , err )
}
var exitErr * exec . ExitError
if ! errors . As ( err , & exitErr ) {
t . Fatalf ( "Check() error = %v, want the Go process error preserved" , err )
}
if ! errors . Is ( err , exitErr ) {
t . Fatal ( "Check() error does not preserve the Go process error identity" )
}
2026-10-10 02:15:13 +00:00
if captured . RepositoryDir != repo . work || captured . ModuleDir != repo . work || captured . ModulePath != "example.com/releasekit-publish-fixture" || captured . Tag != "v1.2.3" || captured . Commit != repo . commit || captured . Version != "v1.2.3" {
t . Fatalf ( "Check() failure candidate = %#v, want captured identity alongside validation error" , captured )
}
2026-10-07 18:34:34 +00:00
}
2026-10-07 23:26:48 +00:00
func TestMarkdownValidationRunsThroughCheckAndPublish ( t * testing . T ) {
addBrokenMarkdownDestination := func ( t * testing . T , repo publishFixture ) {
t . Helper ( )
writeFile ( t , repo . work , "guide.md" , "[missing](missing.md#top)\n" )
gitRun ( t , repo . work , "add" , "guide.md" )
gitRun ( t , repo . work , "commit" , "-m" , "add broken Markdown destination" )
}
assertMarkdownFailure := func ( t * testing . T , err error ) {
t . Helper ( )
var failure * releasekit . ValidationError
if ! errors . As ( err , & failure ) || failure . Check != "markdown-links" {
t . Fatalf ( "operation error = %v, want public markdown-links failure" , err )
}
if len ( failure . Findings ) != 1 || failure . Findings [ 0 ] . Path != "guide.md" || failure . Findings [ 0 ] . Target != "missing.md#top" || failure . Findings [ 0 ] . Line != 1 {
t . Fatalf ( "Markdown findings = %#v, want source location and target" , failure . Findings )
}
}
t . Run ( "check" , func ( t * testing . T ) {
repo := newPublishFixture ( t )
addBrokenMarkdownDestination ( t , repo )
_ , err := releasekit . Check ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
Validation : releasekit . ValidationSpec { Markdown : & releasekit . MarkdownValidationSpec { } } ,
} )
assertMarkdownFailure ( t , err )
} )
t . Run ( "publish" , func ( t * testing . T ) {
repo := newPublishFixture ( t )
addBrokenMarkdownDestination ( t , repo )
gitRun ( t , repo . work , "config" , "--unset" , "remote.origin.mirror" )
gitRun ( t , repo . work , "push" , "origin" , "HEAD:refs/heads/main" )
callbackCalls := 0
_ , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
Validation : releasekit . ValidationSpec { Markdown : & releasekit . MarkdownValidationSpec { } } ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
callbackCalls ++
return nil
} ,
} )
assertMarkdownFailure ( t , err )
if callbackCalls != 0 {
t . Fatalf ( "additional checks called %d times after Markdown failure" , callbackCalls )
}
if tag := gitText ( t , repo . work , "tag" , "--list" , "v1.2.3" ) ; tag != "" {
t . Fatalf ( "failed Markdown validation created tag %q" , tag )
}
} )
}
2026-10-06 17:15:46 +00:00
func TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation ( t * testing . T ) {
repo := newPublishFixture ( t )
suiteRuns := filepath . Join ( t . TempDir ( ) , "suite-runs" )
t . Setenv ( "RELEASEKIT_FIXTURE_SUITE_RUNS" , suiteRuns )
wantErr := errors . New ( "additional product check failed" )
var checks int
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
checks ++
return wantErr
} ,
} )
if ! errors . Is ( err , wantErr ) {
t . Fatalf ( "Publish() error = %v, want wrapped additional-check error" , err )
}
2026-10-07 18:34:34 +00:00
var validationError * releasekit . ValidationError
if errors . As ( err , & validationError ) {
t . Fatalf ( "Publish() callback error was reclassified as validation failure: %v" , err )
}
2026-10-06 17:15:46 +00:00
if runs , readErr := os . ReadFile ( suiteRuns ) ; readErr != nil || string ( runs ) != "ran\n" {
t . Fatalf ( "standard suite runs = %q, error = %v; want one successful run" , runs , readErr )
}
if checks != 1 || result . TagObject != "" || result . State != releasekit . PublicationNotAttempted {
t . Fatalf ( "additional-check failure result=%#v checks=%d" , result , checks )
}
for _ , command := range [ ] [ ] string {
{ "git" , "-C" , repo . work , "show-ref" , "--verify" , "--quiet" , "refs/tags/v1.2.3" } ,
{ "git" , "--git-dir" , repo . bare , "show-ref" , "--verify" , "--quiet" , "refs/tags/v1.2.3" } ,
} {
if _ , err := exec . Command ( command [ 0 ] , command [ 1 : ] ... ) . CombinedOutput ( ) ; err == nil {
t . Fatalf ( "additional-check failure created release tag with %v" , command )
}
}
}
2026-10-05 18:53:44 +00:00
func TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished ( t * testing . T ) {
2026-10-05 18:24:37 +00:00
repo := newPublishFixture ( t )
2026-10-05 18:53:44 +00:00
wrapper := installGitWrapper ( t , repo , "pass" )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
hook := filepath . Join ( repo . bare , "hooks" , "pre-receive" )
if err := os . WriteFile ( hook , [ ] byte ( "#!/bin/sh\ncat >/dev/null\nexit 1\n" ) , 0 o755 ) ; err != nil {
2026-10-05 18:24:37 +00:00
t . Fatal ( err )
}
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-06 17:15:46 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error { return nil } ,
2026-10-05 18:24:37 +00:00
} )
if err == nil {
t . Fatal ( "Publish() succeeded despite rejected push hook" )
}
2026-10-05 18:53:44 +00:00
if result . State != releasekit . PublicationNotPublished || result . Phase != releasekit . PublicationReconciliation || result . TagObject == "" {
2026-10-05 18:24:37 +00:00
t . Fatalf ( "publication result = %#v" , result )
}
2026-10-05 18:53:44 +00:00
if ! strings . Contains ( err . Error ( ) , "failed to push some refs" ) || ! strings . Contains ( err . Error ( ) , "authoritative tag is absent" ) {
t . Fatalf ( "push error was not preserved with absence evidence: %v" , err )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 4 {
t . Fatalf ( "authoritative tag query count = %d, want one reconciliation query" , queries )
}
2026-10-05 18:24:37 +00:00
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" )
}
}
2026-10-05 18:53:44 +00:00
func TestPublishLostAcknowledgmentConfirmsExactRemoteObject ( t * testing . T ) {
repo := newPublishFixture ( t )
wrapper := installGitWrapper ( t , repo , "lost-ack-cancel" )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
finished := make ( chan struct { } )
watcherDone := make ( chan struct { } )
result , err := releasekit . Publish ( ctx , releasekit . ReleaseRequest {
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
2026-10-06 17:15:46 +00:00
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
2026-10-05 18:53:44 +00:00
go func ( ) {
defer close ( watcherDone )
for {
if _ , statErr := os . Stat ( wrapper . pushDone ) ; statErr == nil {
cancel ( )
return
}
select {
case <- finished :
return
case <- time . After ( time . Millisecond ) :
}
}
} ( )
return nil
} ,
} )
close ( finished )
<- watcherDone
if err != nil {
t . Fatal ( err )
}
if result . State != releasekit . PublicationPublished || result . Phase != releasekit . PublicationComplete {
t . Fatalf ( "publication result = %#v" , result )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 4 {
t . Fatalf ( "authoritative tag query count = %d, want one cancellation recovery query" , queries )
}
if got := gitText ( t , repo . bare , "rev-parse" , "refs/tags/v1.2.3" ) ; got != result . TagObject {
t . Fatalf ( "remote tag object = %s, want %s" , got , result . TagObject )
}
}
2026-10-06 16:04:35 +00:00
func TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown ( t * testing . T ) {
repo := newPublishFixture ( t )
base := t . TempDir ( )
emptyRemote := filepath . Join ( base , "empty.git" )
gitRun ( t , base , "init" , "--bare" , emptyRemote )
wrapper := installGitWrapperWithMutationMode ( t , repo , "lost-ack-rewrite" , emptyRemote )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-06 17:15:46 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error { return nil } ,
2026-10-06 16:04:35 +00:00
} )
if err == nil || result . State != releasekit . PublicationUnknown || result . Phase != releasekit . PublicationReconciliation {
t . Fatalf ( "result = %#v, error = %v" , result , err )
}
if ! strings . Contains ( err . Error ( ) , "scoped URL resolves" ) || ! strings . Contains ( err . Error ( ) , "lost acknowledgment" ) {
t . Fatalf ( "error does not preserve endpoint verification and push failures: %v" , err )
}
if ! errors . Is ( err , git . ErrEndpointChanged ) || ! errors . Is ( err , git . ErrRemoteStateUnreadable ) {
t . Fatalf ( "endpoint verification error identity was not preserved: %v" , err )
}
var exitErr * exec . ExitError
if ! errors . As ( err , & exitErr ) || exitErr . ExitCode ( ) != 77 {
t . Fatalf ( "push cause identity was not preserved: %v" , err )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 3 {
t . Fatalf ( "authoritative tag query count = %d, want no tag query after endpoint verification failed" , queries )
}
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 got := gitText ( t , repo . bare , "rev-parse" , "refs/tags/v1.2.3" ) ; got != result . TagObject {
t . Fatalf ( "original remote tag object = %s, want published object %s" , got , result . TagObject )
}
if got := gitText ( t , repo . bare , "rev-parse" , "refs/tags/v1.2.3^{}" ) ; got != repo . commit {
t . Fatalf ( "original remote tag peels to %s, want candidate commit %s" , got , repo . commit )
}
}
2026-10-05 18:53:44 +00:00
func TestPublishConflictingRemoteTagIsNotReportedAsAbsent ( t * testing . T ) {
repo := newPublishFixture ( t )
wrapper := installGitWrapper ( t , repo , "pass" )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
hook := filepath . Join ( repo . work , ".git" , "hooks" , "pre-push" )
conflictingObject := gitText ( t , repo . work , "rev-parse" , "refs/tags/v0.9.0" )
script := fmt . Sprintf ( "#!/bin/sh\n%s --git-dir=%s update-ref refs/tags/v1.2.3 %s\nexit 1\n" , shellQuote ( mustLookPath ( t , "git" ) ) , shellQuote ( repo . bare ) , conflictingObject )
if err := os . WriteFile ( hook , [ ] byte ( script ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-06 17:15:46 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error { return nil } ,
2026-10-05 18:53:44 +00:00
} )
if err == nil || result . State != releasekit . PublicationConflict || result . Phase != releasekit . PublicationReconciliation {
t . Fatalf ( "result = %#v, error = %v" , result , err )
}
if ! strings . Contains ( err . Error ( ) , "authoritative tag identifies object" ) || ! strings . Contains ( err . Error ( ) , "failed to push some refs" ) {
t . Fatalf ( "error does not distinguish conflict and preserve push failure: %v" , err )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 4 {
t . Fatalf ( "authoritative tag query count = %d, want one reconciliation query" , queries )
}
}
func TestPublishStopsWhenLocalTagChangesBeforePush ( t * testing . T ) {
repo := newPublishFixture ( t )
gitRun ( t , repo . work , "-c" , "tag.gpgSign=false" , "tag" , "--annotate" , "--message" , "different tag" , "v0.8.0" , repo . commit )
conflictingObject := gitText ( t , repo . work , "rev-parse" , "refs/tags/v0.8.0" )
wrapper := installGitWrapperWithMutation ( t , repo , conflictingObject )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-06 17:15:46 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error { return nil } ,
2026-10-05 18:53:44 +00:00
} )
if err == nil || result . State != releasekit . PublicationNotPublished || result . Phase != releasekit . PublicationTagCreation {
t . Fatalf ( "result = %#v, error = %v" , result , err )
}
if got := gitText ( t , repo . work , "rev-parse" , "refs/tags/v1.2.3" ) ; got != conflictingObject {
t . Fatalf ( "changed local tag object = %s, want %s" , got , conflictingObject )
}
if result . TagObject == "" || result . TagObject == conflictingObject {
t . Fatalf ( "captured tag object = %q, want the prior object; publish error: %v" , result . TagObject , err )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 2 {
t . Fatalf ( "remote query count = %d, want only the pre-validation and prepublication guards" , queries )
}
if _ , err := exec . Command ( "git" , "--git-dir" , repo . bare , "show-ref" , "--verify" , "--quiet" , "refs/tags/v1.2.3" ) . CombinedOutput ( ) ; err == nil {
t . Fatal ( "changed local tag was pushed" )
}
}
func TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError ( t * testing . T ) {
repo := newPublishFixture ( t )
wrapper := installGitWrapper ( t , repo , "query-failure" )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
hook := filepath . Join ( repo . bare , "hooks" , "pre-receive" )
if err := os . WriteFile ( hook , [ ] byte ( "#!/bin/sh\ncat >/dev/null\nexit 1\n" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-06 17:15:46 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error { return nil } ,
2026-10-05 18:53:44 +00:00
} )
if err == nil || result . State != releasekit . PublicationUnknown || result . Phase != releasekit . PublicationReconciliation {
t . Fatalf ( "result = %#v, error = %v" , result , err )
}
if ! strings . Contains ( err . Error ( ) , "reconciliation unavailable" ) || ! strings . Contains ( err . Error ( ) , "failed to push some refs" ) {
t . Fatalf ( "error does not preserve both query and push failure: %v" , err )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 4 {
t . Fatalf ( "authoritative tag query count = %d, want one failed reconciliation query" , queries )
}
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 )
}
var exitErr * exec . ExitError
if ! errors . As ( err , & exitErr ) || exitErr . ExitCode ( ) == 0 {
t . Fatalf ( "push cause was not preserved in error chain: %v" , err )
}
}
func TestPublishUninterpretableReconciliationRemainsUnknown ( t * testing . T ) {
repo := newPublishFixture ( t )
other := gitText ( t , repo . work , "rev-parse" , "refs/tags/v0.9.0" )
wrapper := installGitWrapperWithMutationMode ( t , repo , "query-conflict" , other )
t . Setenv ( "PATH" , wrapper . bin + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
hook := filepath . Join ( repo . work , ".git" , "hooks" , "pre-push" )
if err := os . WriteFile ( hook , [ ] byte ( "#!/bin/sh\nexit 1\n" ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
result , err := releasekit . Publish ( context . Background ( ) , releasekit . ReleaseRequest {
2026-10-06 17:15:46 +00:00
RepositoryDir : repo . work ,
Product : "Sample App" ,
Version : "v1.2.3" ,
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error { return nil } ,
2026-10-05 18:53:44 +00:00
} )
if err == nil || result . State != releasekit . PublicationUnknown || result . Phase != releasekit . PublicationReconciliation {
t . Fatalf ( "result = %#v, error = %v" , result , err )
}
if ! strings . Contains ( err . Error ( ) , "inconsistent exact ref records" ) || ! strings . Contains ( err . Error ( ) , "failed to push some refs" ) {
t . Fatalf ( "error does not preserve uninterpretable remote state and push failure: %v" , err )
}
if queries := countLines ( t , wrapper . queryLog ) ; queries != 4 {
t . Fatalf ( "authoritative tag query count = %d, want one uninterpretable reconciliation query" , queries )
}
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 )
}
}
2026-10-05 18:24:37 +00:00
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" ,
2026-10-06 17:15:46 +00:00
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
2026-10-05 18:24:37 +00:00
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" ,
2026-10-06 17:15:46 +00:00
AdditionalChecks : func ( context . Context , releasekit . Candidate ) error {
2026-10-05 18:24:37 +00:00
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
}
2026-10-05 18:53:44 +00:00
type gitWrapper struct {
bin string
queryLog string
pushDone string
realGit string
}
func installGitWrapper ( t * testing . T , repo publishFixture , mode string ) gitWrapper {
t . Helper ( )
return installGitWrapperWithMutationMode ( t , repo , mode , "" )
}
2026-10-10 02:18:02 +00:00
func addNestedReleaseModule ( t * testing . T , repository , moduleDir , modulePath , version string ) {
t . Helper ( )
writeFile ( t , repository , moduleDir + "/go.mod" , "module " + modulePath + "\n\ngo 1.25.0\n" )
2026-10-10 02:43:23 +00:00
writeFile ( t , repository , moduleDir + "/fixture_test.go" , "package fixture\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n" )
2026-10-10 02:18:02 +00:00
writeFile ( t , repository , moduleDir + "/docs/releases/" + version + ".md" , "# Sample App " + version + "\n\nNested release.\n" )
}
func installTagQueryFailureWrapper ( t * testing . T , tag string ) string {
t . Helper ( )
realGit , err := exec . LookPath ( "git" )
if err != nil {
t . Fatal ( err )
}
bin := filepath . Join ( t . TempDir ( ) , "bin" )
if err := os . MkdirAll ( bin , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
marker := filepath . Join ( t . TempDir ( ) , "push-attempted" )
contents := fmt . Sprintf ( ` # ! / bin / sh
REAL_GIT = % s
MARKER = % s
TAG_REF = % s
ARGS = " $* "
IS_PUSH = 0
IS_LS_REMOTE = 0
for ARG in "$@" ; do
[ "$ARG" = "push" ] && IS_PUSH = 1
[ "$ARG" = "ls-remote" ] && IS_LS_REMOTE = 1
done
if [ "$IS_LS_REMOTE" = "1" ] && [ - f "$MARKER" ] ; then
case "$ARGS" in * "$TAG_REF" * ) echo "reconciliation unavailable" > & 2 ; exit 77 ; ; esac
fi
if [ "$IS_PUSH" = "1" ] ; then
"$REAL_GIT" "$@"
STATUS = $ ?
: > "$MARKER"
exit "$STATUS"
fi
exec "$REAL_GIT" "$@"
` , shellQuote ( realGit ) , shellQuote ( marker ) , shellQuote ( "refs/tags/" + tag ) )
if err := os . WriteFile ( filepath . Join ( bin , "git" ) , [ ] byte ( contents ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
return bin
}
2026-10-05 18:53:44 +00:00
func installGitWrapperWithMutation ( t * testing . T , repo publishFixture , object string ) gitWrapper {
t . Helper ( )
return installGitWrapperWithMutationMode ( t , repo , "mutate-tag" , object )
}
func installGitWrapperWithMutationMode ( t * testing . T , repo publishFixture , mode , mutationObject string ) gitWrapper {
t . Helper ( )
realGit , err := exec . LookPath ( "git" )
if err != nil {
t . Fatal ( err )
}
bin := filepath . Join ( t . TempDir ( ) , "bin" )
if err := os . MkdirAll ( bin , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
queryLog := filepath . Join ( t . TempDir ( ) , "tag-queries.log" )
pushDone := filepath . Join ( t . TempDir ( ) , "push-attempted" )
contents := fmt . Sprintf ( ` # ! / bin / sh
REAL_GIT = % s
QUERY_LOG = % s
PUSH_DONE = % s
MODE = % s
MUTATE_OBJECT = % s
ARGS = " $* "
IS_PUSH = 0
IS_LS_REMOTE = 0
for ARG in "$@" ; do
[ "$ARG" = "push" ] && IS_PUSH = 1
[ "$ARG" = "ls-remote" ] && IS_LS_REMOTE = 1
done
if [ "$IS_LS_REMOTE" = "1" ] ; then
case "$ARGS" in * refs / tags / v1 .2 .3 * ) echo query >> "$QUERY_LOG" ; ; esac
if [ "$MODE" = "query-failure" ] && [ - f "$PUSH_DONE" ] ; then
case "$ARGS" in * refs / tags / v1 .2 .3 * ) echo "reconciliation unavailable" > & 2 ; exit 77 ; ; esac
fi
if [ "$MODE" = "query-conflict" ] && [ - f "$PUSH_DONE" ] ; then
case "$ARGS" in
* refs / tags / v1 .2 .3 * )
OBJECT = $ ( "$REAL_GIT" rev - parse refs / tags / v1 .2 .3 )
printf ' % % s \ trefs / tags / v1 .2 .3 \ n ' "$OBJECT"
printf ' % % s \ trefs / tags / v1 .2 .3 \ n ' "$MUTATE_OBJECT"
exit 0
; ;
esac
fi
fi
if [ "$IS_PUSH" = "1" ] ; then
"$REAL_GIT" "$@"
STATUS = $ ?
: > "$PUSH_DONE"
if [ "$MODE" = "lost-ack-cancel" ] && [ "$STATUS" = "0" ] ; then kill - STOP $ $ ; fi
2026-10-06 16:04:35 +00:00
if [ "$MODE" = "lost-ack-rewrite" ] && [ "$STATUS" = "0" ] ; then
"$REAL_GIT" config -- local "url.\"$MUTATE_OBJECT\".insteadOf" % s
echo "lost acknowledgment after tag was accepted" > & 2
exit 77
fi
2026-10-05 18:53:44 +00:00
exit "$STATUS"
fi
if [ "$MODE" = "mutate-tag" ] ; then
case "$ARGS" in
* "cat-file tag refs/tags/v1.2.3" * )
"$REAL_GIT" "$@"
STATUS = $ ?
[ "$STATUS" = "0" ] && "$REAL_GIT" update - ref refs / tags / v1 .2 .3 "$MUTATE_OBJECT"
exit "$STATUS"
; ;
esac
fi
exec "$REAL_GIT" "$@"
2026-10-06 16:04:35 +00:00
` , shellQuote ( realGit ) , shellQuote ( queryLog ) , shellQuote ( pushDone ) , shellQuote ( mode ) , shellQuote ( mutationObject ) , shellQuote ( repo . bare ) )
2026-10-05 18:53:44 +00:00
if err := os . WriteFile ( filepath . Join ( bin , "git" ) , [ ] byte ( contents ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
return gitWrapper { bin : bin , queryLog : queryLog , pushDone : pushDone , realGit : realGit }
}
func countLines ( t * testing . T , path string ) int {
t . Helper ( )
contents , err := os . ReadFile ( path )
if os . IsNotExist ( err ) {
return 0
}
if err != nil {
t . Fatal ( err )
}
return len ( strings . Fields ( string ( contents ) ) )
}
func mustLookPath ( t * testing . T , name string ) string {
t . Helper ( )
path , err := exec . LookPath ( name )
if err != nil {
t . Fatal ( err )
}
return path
}
func shellQuote ( value string ) string {
return "'" + strings . ReplaceAll ( value , "'" , "'\\''" ) + "'"
}
2026-10-09 01:02:05 +00:00
func newPublishFixture ( t testing . TB ) publishFixture {
2026-10-05 18:24:37 +00:00
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" )
2026-10-06 17:15:46 +00:00
writeFile ( t , work , "go.mod" , "module example.com/releasekit-publish-fixture\n\ngo 1.25.0\n" )
writeFile ( t , work , "fixture_test.go" , ` package fixture
import (
"os"
"testing"
)
func TestFixture ( t * testing . T ) {
if path := os . Getenv ( "RELEASEKIT_FIXTURE_SUITE_RUNS" ) ; path != "" {
file , err := os . OpenFile ( path , os . O_CREATE | os . O_APPEND | os . O_WRONLY , 0 o600 )
if err != nil {
t . Fatal ( err )
}
if _ , err := file . WriteString ( "ran\n" ) ; err != nil {
_ = file . Close ( )
t . Fatal ( err )
}
if err := file . Close ( ) ; err != nil {
t . Fatal ( err )
}
}
if os . Getenv ( "RELEASEKIT_FAIL_FIXTURE_SUITE" ) == "1" {
t . Fatal ( "fixture standard suite failure" )
}
}
2026-10-07 22:10:34 +00:00
` )
writeFile ( t , work , "race_fixture_test.go" , ` //go:build race
package fixture
import (
"os"
"testing"
)
func TestRaceBuildSelection ( t * testing . T ) {
marker := os . Getenv ( "RELEASEKIT_RACE_MARKER" )
if marker == "" {
return
}
if err := os . WriteFile ( marker , [ ] byte ( "race enabled" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
}
2026-10-06 17:15:46 +00:00
` )
2026-10-05 18:24:37 +00:00
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 }
}
2026-10-09 01:02:05 +00:00
func gitRun ( t testing . TB , dir string , args ... string ) {
2026-10-05 18:24:37 +00:00
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 )
}
}
2026-10-09 01:02:05 +00:00
func gitText ( t testing . TB , dir string , args ... string ) string {
2026-10-05 18:24:37 +00:00
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 )
}
2026-10-09 01:02:05 +00:00
func writeFile ( t testing . TB , root , name , contents string ) {
2026-10-05 18:24:37 +00:00
t . Helper ( )
path := filepath . Join ( root , filepath . FromSlash ( name ) )
if err := os . MkdirAll ( filepath . Dir ( path ) , 0 o755 ) ; err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( path , [ ] byte ( contents ) , 0 o644 ) ; err != nil {
t . Fatal ( err )
}
}