2026-10-05 17:25:11 +00:00
package cli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParse ( t * testing . T ) {
invocation , err := Parse ( [ ] string { "version" } )
if err != nil {
t . Fatal ( err )
}
if invocation . Command != VersionCommand {
t . Fatalf ( "Parse() command = %q, want %q" , invocation . Command , VersionCommand )
}
2026-10-05 17:47:50 +00:00
check , err := Parse ( [ ] string { "check" , "--repo" , "repository with spaces" , "--config" , "profiles/release config.json" , "v1.2.3" } )
if err != nil {
t . Fatal ( err )
}
if check . Command != CheckAction || check . Repository != "repository with spaces" || check . ConfigPath != "profiles/release config.json" || check . Version != "v1.2.3" {
t . Fatalf ( "Parse(check) = %#v" , check )
}
2026-10-05 19:04:55 +00:00
publish , err := Parse ( [ ] string { "publish" , "--repo" , "candidate" , "--config" , "releasekit.json" , "v1.2.4" } )
if err != nil {
t . Fatal ( err )
}
if publish . Command != PublishAction || publish . Repository != "candidate" || publish . ConfigPath != "releasekit.json" || publish . Version != "v1.2.4" {
t . Fatalf ( "Parse(publish) = %#v" , publish )
}
2026-10-05 19:38:50 +00:00
build , err := Parse ( [ ] string { "build" , "--repo" , "candidate" , "--config" , "releasekit.json" , "--output" , "output with spaces" , "v1.2.5" } )
if err != nil {
t . Fatal ( err )
}
if build . Command != BuildAction || build . Repository != "candidate" || build . ConfigPath != "releasekit.json" || build . OutputDir != "output with spaces" || build . Version != "v1.2.5" {
t . Fatalf ( "Parse(build) = %#v" , build )
}
2026-10-05 20:39:33 +00:00
deliver , err := Parse ( [ ] string { "deliver" , "--repo" , "candidate" , "--config" , "releasekit.json" , "--assets" , "assets with spaces" , "v1.2.6" } )
if err != nil {
t . Fatal ( err )
}
if deliver . Command != DeliveryAction || deliver . Repository != "candidate" || deliver . ConfigPath != "releasekit.json" || deliver . AssetsDir != "assets with spaces" || deliver . Version != "v1.2.6" {
t . Fatalf ( "Parse(deliver) = %#v" , deliver )
}
2026-10-06 19:26:54 +00:00
ciDelivery , err := Parse ( [ ] string { "ci-deliver" , "--repo" , "candidate" , "--config" , "profiles/release config.json" , "--remote" , "upstream" , "--commit" , "0123456789abcdef0123456789abcdef01234567" , "v1.2.7" } )
if err != nil {
t . Fatal ( err )
}
if ciDelivery . Command != CIDeliveryAction || ciDelivery . Repository != "candidate" || ciDelivery . ConfigPath != "profiles/release config.json" || ciDelivery . Remote != "upstream" || ciDelivery . Commit != "0123456789abcdef0123456789abcdef01234567" || ciDelivery . Version != "v1.2.7" {
t . Fatalf ( "Parse(ci-deliver) = %#v" , ciDelivery )
}
ciDefault , err := Parse ( [ ] string { "ci-deliver" , "--commit" , "0123456789abcdef0123456789abcdef01234567" , "v1.2.8" } )
if err != nil || ciDefault . Remote != "origin" {
t . Fatalf ( "Parse(ci-deliver defaults) = %#v, error = %v" , ciDefault , err )
}
if _ , err := Parse ( [ ] string { "ci-deliver" , "v1.2.3" } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "--commit" ) {
t . Fatalf ( "Parse(ci-deliver without commit) error = %v, want required commit" , err )
}
if _ , err := Parse ( [ ] string { "ci-deliver" , "--commit" , "abc123" , "v1.2.3" } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "full Git object ID" ) {
t . Fatalf ( "Parse(ci-deliver with abbreviated commit) error = %v, want full object ID error" , err )
}
2026-10-05 17:47:50 +00:00
if _ , err := Parse ( [ ] string { "check" , "v1.2.3" , "--repo" , "repository" } ) ; err == nil {
t . Fatal ( "Parse accepted a flag after the version" )
}
2026-10-06 19:26:54 +00:00
if _ , err := Parse ( [ ] string { "ci-deliver" , "--commit" , "0123456789abcdef0123456789abcdef01234567" , "v1.2.3" , "--repo" , "repository" } ) ; err == nil {
t . Fatal ( "Parse accepted a ci-deliver flag after the version" )
}
2026-10-05 17:25:11 +00:00
if _ , err := Parse ( nil ) ; err == nil || ! strings . Contains ( err . Error ( ) , "command is required" ) {
t . Fatalf ( "Parse(nil) error = %v, want missing-command error" , err )
}
2026-10-05 19:04:55 +00:00
if _ , err := Parse ( [ ] string { "unknown" } ) ; err == nil || ! strings . Contains ( err . Error ( ) , "unknown command" ) {
t . Fatalf ( "Parse(unknown) error = %v, want unknown-command error" , err )
2026-10-05 17:25:11 +00:00
}
}
func TestLoadSettingsResolvesPathsAndPreservesArguments ( t * testing . T ) {
repoDir := filepath . Join ( t . TempDir ( ) , "repository with spaces" )
profileDir := filepath . Join ( repoDir , "profiles with spaces" )
if err := os . MkdirAll ( profileDir , 0 o700 ) ; err != nil {
t . Fatal ( err )
}
want := Settings {
Product : "Example Product" ,
Remote : "upstream" ,
Checks : [ ] CheckCommand { {
Args : [ ] string { "sh" , "scripts/check release.sh" , "two words" } ,
Env : map [ string ] string { "RELEASE_MODE" : "candidate check" } ,
} } ,
}
contents , err := json . Marshal ( want )
if err != nil {
t . Fatal ( err )
}
if err := os . WriteFile ( filepath . Join ( profileDir , "release profile.json" ) , contents , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
got , err := LoadSettings ( repoDir , filepath . Join ( "profiles with spaces" , "release profile.json" ) )
if err != nil {
t . Fatal ( err )
}
if got . Product != want . Product || got . Remote != want . Remote || len ( got . Checks ) != 1 {
t . Fatalf ( "LoadSettings() = %#v, want product, remote, and one check" , got )
}
if got . Checks [ 0 ] . Args [ 1 ] != "scripts/check release.sh" || got . Checks [ 0 ] . Args [ 2 ] != "two words" {
t . Fatalf ( "check arguments were changed: %#v" , got . Checks [ 0 ] . Args )
}
if got . Checks [ 0 ] . Env [ "RELEASE_MODE" ] != "candidate check" {
t . Fatalf ( "check environment = %#v" , got . Checks [ 0 ] . Env )
}
}
func TestLoadSettingsDefaultsRemoteAndConfigPath ( t * testing . T ) {
repoDir := t . TempDir ( )
contents := ` { "product":"Example"} `
profilePath := filepath . Join ( repoDir , "releasekit.json" )
if err := os . WriteFile ( profilePath , [ ] byte ( contents ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
for _ , test := range [ ] struct {
name string
path string
} {
{ name : "default path" } ,
{ name : "absolute path" , path : profilePath } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
settings , err := LoadSettings ( repoDir , test . path )
if err != nil {
t . Fatal ( err )
}
if settings . Remote != "origin" {
t . Fatalf ( "default remote = %q, want origin" , settings . Remote )
}
} )
}
}
2026-10-07 22:18:47 +00:00
func TestLoadSettingsReadsGoValidation ( t * testing . T ) {
2026-10-07 22:10:34 +00:00
repoDir := t . TempDir ( )
profilePath := filepath . Join ( repoDir , "releasekit.json" )
2026-10-07 22:39:15 +00:00
if err := os . WriteFile ( profilePath , [ ] byte ( ` { "product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation": { "go_format":true,"go": { "race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}} ` ) , 0 o600 ) ; err != nil {
2026-10-07 22:10:34 +00:00
t . Fatal ( err )
}
settings , err := LoadSettings ( repoDir , "" )
if err != nil {
t . Fatal ( err )
}
2026-10-07 22:39:15 +00:00
if settings . Validation == nil || ! settings . Validation . GoFormat || ! settings . Validation . Go . Race || ! settings . Validation . Go . FullVet {
t . Fatalf ( "validation settings = %#v, want Go formatting, race, and full vet enabled" , settings . Validation )
2026-10-07 22:10:34 +00:00
}
2026-10-07 22:27:31 +00:00
if got , want := strings . Join ( settings . Validation . Go . CompileTargets , "," ) , "linux/amd64,linux/arm64,darwin/arm64" ; got != want {
t . Fatalf ( "compile targets = %q, want %q" , got , want )
}
2026-10-07 22:31:58 +00:00
if settings . ExpectedPushURL != "ssh://git.example/owner/repo.git" {
t . Fatalf ( "expected push URL = %q" , settings . ExpectedPushURL )
}
2026-10-07 22:10:34 +00:00
}
2026-10-05 17:25:11 +00:00
func TestExampleProfileLoads ( t * testing . T ) {
repoDir , err := filepath . Abs ( filepath . Join ( ".." , ".." , ".." , ".." ) )
if err != nil {
t . Fatal ( err )
}
settings , err := LoadSettings ( repoDir , filepath . Join ( "examples" , "releasekit.json" ) )
if err != nil {
t . Fatal ( err )
}
2026-10-06 17:15:46 +00:00
if settings . Product != "Example" || settings . Remote != "origin" || len ( settings . Checks ) != 0 {
2026-10-05 17:25:11 +00:00
t . Fatalf ( "example settings = %#v" , settings )
}
}
2026-10-05 19:38:50 +00:00
func TestBuildProfileLoads ( t * testing . T ) {
repoDir := t . TempDir ( )
path := filepath . Join ( repoDir , "profile.json" )
if err := os . WriteFile ( path , [ ] byte ( ` { "product":"Example","build": { "main_package":"./cmd/app","binary_name":"example","targets":["linux/amd64","darwin/arm64"]}} ` ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
settings , err := LoadSettings ( repoDir , path )
if err != nil {
t . Fatal ( err )
}
if settings . Build == nil || settings . Build . MainPackage != "./cmd/app" || settings . Build . BinaryName != "example" || len ( settings . Build . Targets ) != 2 {
t . Fatalf ( "build settings = %#v" , settings . Build )
}
repoDir , err = filepath . Abs ( filepath . Join ( ".." , ".." , ".." , ".." ) )
if err != nil {
t . Fatal ( err )
}
example , err := LoadSettings ( repoDir , filepath . Join ( "examples" , "releasekit-binary.json" ) )
if err != nil {
t . Fatal ( err )
}
if example . Build == nil || len ( example . Build . Targets ) != 3 {
t . Fatalf ( "binary example settings = %#v" , example . Build )
}
}
2026-10-05 17:25:11 +00:00
func TestLoadSettingsRejectsInvalidProfiles ( t * testing . T ) {
tests := [ ] struct {
name string
content string
want string
} {
{ name : "missing product" , content : ` { } ` , want : "product is required" } ,
2026-10-05 20:39:33 +00:00
{ name : "unknown top-level field" , content : ` { "product":"Example","future": { }} ` , want : "unknown field" } ,
2026-10-05 19:38:50 +00:00
{ name : "build package required" , content : ` { "product":"Example","build": { "binary_name":"example","targets":["linux/amd64"]}} ` , want : "build.main_package is required" } ,
{ name : "build target format" , content : ` { "product":"Example","build": { "main_package":".","binary_name":"example","targets":["linux"]}} ` , want : "must be GOOS/GOARCH" } ,
2026-10-07 22:27:31 +00:00
{ name : "compile target format" , content : ` { "product":"Example","validation": { "go": { "compile_targets":["linux"]}}} ` , want : "validation.go.compile_targets[0] must be GOOS/GOARCH" } ,
{ name : "compile target missing architecture" , content : ` { "product":"Example","validation": { "go": { "compile_targets":["linux/"]}}} ` , want : "validation.go.compile_targets[0] must be GOOS/GOARCH" } ,
{ name : "compile target missing operating system" , content : ` { "product":"Example","validation": { "go": { "compile_targets":["/amd64"]}}} ` , want : "validation.go.compile_targets[0] must be GOOS/GOARCH" } ,
{ name : "compile target repeated separator" , content : ` { "product":"Example","validation": { "go": { "compile_targets":["linux/amd64/arm64"]}}} ` , want : "validation.go.compile_targets[0] must be GOOS/GOARCH" } ,
2026-10-05 19:38:50 +00:00
{ name : "unsafe binary name" , content : ` { "product":"Example","build": { "main_package":".","binary_name":"../example","targets":["linux/amd64"]}} ` , want : "filename component" } ,
2026-10-05 17:25:11 +00:00
{ name : "unknown command field" , content : ` { "product":"Example","checks":[ { "args":["true"],"shell":true}]} ` , want : "unknown field" } ,
{ name : "command is required" , content : ` { "product":"Example","checks":[ { "args":[]}]} ` , want : "must include a command" } ,
{ name : "multiple JSON values" , content : ` { "product":"Example"} { "product":"Other"} ` , want : "multiple JSON values" } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
repoDir := t . TempDir ( )
path := filepath . Join ( repoDir , "profile.json" )
if err := os . WriteFile ( path , [ ] byte ( test . content ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
if _ , err := LoadSettings ( repoDir , path ) ; err == nil || ! strings . Contains ( err . Error ( ) , test . want ) {
t . Fatalf ( "LoadSettings() error = %v, want text %q" , err , test . want )
}
} )
}
}
func TestRunChecksUsesConfiguredArgumentsEnvironmentAndDirectory ( t * testing . T ) {
workdir := filepath . Join ( t . TempDir ( ) , "repo with spaces" )
if err := os . Mkdir ( workdir , 0 o700 ) ; err != nil {
t . Fatal ( err )
}
var stdout , stderr bytes . Buffer
err := RunChecks ( context . Background ( ) , workdir , [ ] CheckCommand { {
Args : [ ] string { helperExecutable ( t ) , "-test.run=TestConfiguredCommandHelper" , "--" , "argument with spaces" } ,
2026-10-05 17:47:50 +00:00
Env : map [ string ] string {
"RELEASEKIT_CONFIGURED_HELPER" : "1" ,
"RELEASEKIT_CONFIGURED_VALUE" : "selected value" ,
"RELEASEKIT_VERSION" : "profile value" ,
} ,
} } , map [ string ] string { "RELEASEKIT_VERSION" : "captured version" , "RELEASEKIT_CANDIDATE" : "captured commit" } , & stdout , & stderr )
2026-10-05 17:25:11 +00:00
if err != nil {
t . Fatal ( err )
}
2026-10-05 17:47:50 +00:00
want := fmt . Sprintf ( "%s|argument with spaces|selected value|captured version|captured commit" , workdir )
2026-10-05 17:25:11 +00:00
if stdout . String ( ) != want {
t . Fatalf ( "stdout = %q, want %q" , stdout . String ( ) , want )
}
}
func TestConfiguredCommandHelper ( t * testing . T ) {
if os . Getenv ( "RELEASEKIT_CONFIGURED_HELPER" ) != "1" {
return
}
workdir , err := os . Getwd ( )
if err != nil {
os . Exit ( 9 )
}
2026-10-05 17:47:50 +00:00
fmt . Fprintf ( os . Stdout , "%s|%s|%s|%s|%s" , workdir , os . Args [ len ( os . Args ) - 1 ] , os . Getenv ( "RELEASEKIT_CONFIGURED_VALUE" ) , os . Getenv ( "RELEASEKIT_VERSION" ) , os . Getenv ( "RELEASEKIT_CANDIDATE" ) )
2026-10-05 17:25:11 +00:00
os . Exit ( 0 )
}
func helperExecutable ( t * testing . T ) string {
t . Helper ( )
executable , err := os . Executable ( )
if err != nil {
t . Fatal ( err )
}
return executable
}