2026-09-23 15:03:20 +00:00
package scripts
import (
2026-10-07 13:58:07 +00:00
"encoding/json"
2026-10-07 14:44:36 +00:00
"maps"
2026-09-23 15:03:20 +00:00
"os"
"os/exec"
"path/filepath"
2026-10-07 13:58:07 +00:00
"reflect"
2026-10-07 14:50:41 +00:00
"slices"
"strconv"
2026-09-23 15:03:20 +00:00
"strings"
"testing"
2026-10-07 23:45:27 +00:00
"gopkg.in/yaml.v3"
2026-09-23 15:03:20 +00:00
)
2026-10-07 13:58:07 +00:00
const goStub = ` # ! / usr / bin / env bash
set - euo pipefail
2026-10-07 23:45:27 +00:00
case $ { 1 : ? missing Go command } in
list )
log = $ RELEASEKIT_LIST_LOG
printf ' % s \ 0 ' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$log"
modfile =
for argument in "$@" ; do
case $ argument in - modfile = * ) modfile = $ { argument # - modfile = } ; ; esac
done
[ [ - n $ modfile && - f $ modfile && - f $ { modfile % . mod } . sum ] ]
cp "$modfile" "$RELEASEKIT_MODULE_LOG"
cp "${modfile%.mod}.sum" "$RELEASEKIT_SUM_LOG"
if [ [ $ { RELEASEKIT_BOOTSTRAP_STATUS : - 0 } != 0 ] ] ; then
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
fi
printf ' % s \ n ' v0 .1 .0
; ;
build )
printf ' % s \ 0 ' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_BUILD_LOG"
if [ [ $ { RELEASEKIT_BOOTSTRAP_STATUS : - 0 } != 0 ] ] ; then
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
fi
output =
while ( ( $ # ) ) ; do
case $ 1 in
- o )
shift
output = $ { 1 : ? missing output path }
; ;
- o = * ) output = $ { 1 # - o = } ; ;
esac
2026-10-07 13:58:07 +00:00
shift
2026-10-07 23:45:27 +00:00
done
[ [ - n $ output ] ]
cat > "$output" << ' RELEASEKIT_TOOL '
2026-10-07 13:58:07 +00:00
# ! / usr / bin / env bash
set - euo pipefail
printf ' % s \ 0 ' "$PWD" "$@" > "$RELEASEKIT_ARGS_LOG"
exit "${RELEASEKIT_TOOL_STATUS:-0}"
RELEASEKIT_TOOL
2026-10-07 23:45:27 +00:00
chmod + x "$output"
; ;
* ) exit 64 ; ;
esac
2026-10-07 13:58:07 +00:00
`
func TestReleaseWrapperForwardsNativeArgumentsAndStatus ( t * testing . T ) {
2026-10-07 14:50:41 +00:00
for _ , toolStatus := range [ ] int { 0 , 3 } {
t . Run ( "status " + strconv . Itoa ( toolStatus ) , func ( t * testing . T ) {
2026-10-07 13:58:07 +00:00
layout := newReleaseTestLayout ( t )
args := [ ] string { "publish" , "v4.2.0" , "argument with spaces" }
cmd := exec . Command ( "bash" , append ( [ ] string { layout . wrapper } , args ... ) ... )
cmd . Dir = layout . caller
cmd . Env = layout . environment ( map [ string ] string {
"RELEASEKIT_ARGS_LOG" : layout . argsLog ,
"RELEASEKIT_BUILD_LOG" : layout . buildLog ,
2026-10-07 23:45:27 +00:00
"RELEASEKIT_LIST_LOG" : layout . listLog ,
"RELEASEKIT_MODULE_LOG" : layout . moduleLog ,
"RELEASEKIT_SUM_LOG" : layout . sumLog ,
2026-10-07 14:50:41 +00:00
"RELEASEKIT_TOOL_STATUS" : strconv . Itoa ( toolStatus ) ,
2026-10-07 13:58:07 +00:00
} )
output , err := cmd . CombinedOutput ( )
2026-10-07 14:50:41 +00:00
if got := commandStatus ( err ) ; got != toolStatus {
t . Fatalf ( "exit=%d, want %d; output=%q, error=%v" , got , toolStatus , output , err )
2026-09-23 15:03:20 +00:00
}
2026-10-07 13:58:07 +00:00
if len ( output ) != 0 {
t . Fatalf ( "unexpected output: %q" , output )
2026-09-23 15:03:20 +00:00
}
2026-10-07 13:58:07 +00:00
2026-10-07 23:45:27 +00:00
listInvocation := readNULArguments ( t , layout . listLog )
2026-10-07 13:58:07 +00:00
buildInvocation := readNULArguments ( t , layout . buildLog )
2026-10-07 23:45:27 +00:00
for name , invocation := range map [ string ] [ ] string { "list" : listInvocation , "build" : buildInvocation } {
if len ( invocation ) < 4 || invocation [ 0 ] != layout . repo {
t . Fatalf ( "%s invocation=%q, want repository %q" , name , invocation , layout . repo )
}
if invocation [ 1 ] != "GOWORK=off" || invocation [ 2 ] != "GOTOOLCHAIN=local" {
t . Fatalf ( "%s environment=%q" , name , invocation [ 1 : 3 ] )
}
2026-09-23 15:03:20 +00:00
}
2026-10-07 23:45:27 +00:00
listArgs := listInvocation [ 3 : ]
2026-10-07 13:58:07 +00:00
buildArgs := buildInvocation [ 3 : ]
2026-10-07 23:45:27 +00:00
modfile := argumentValue ( listArgs , "-modfile=" )
if listArgs [ 0 ] != "list" || ! slices . Contains ( listArgs , "-mod=mod" ) || ! slices . Contains ( listArgs , "-m" ) || ! slices . Contains ( listArgs , "gitea.maximumdirect.net/eric/ReleaseKit" ) {
t . Fatalf ( "unexpected module-selection arguments: %q" , listArgs )
}
if modfile == "" || argumentValue ( buildArgs , "-modfile=" ) != modfile || ! slices . Contains ( buildArgs , "-mod=mod" ) {
t . Fatalf ( "list/build did not share alternate module metadata: list=%q build=%q" , listArgs , buildArgs )
}
if buildArgs [ 0 ] != "build" || ! slices . Contains ( buildArgs , "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit" ) {
2026-10-07 13:58:07 +00:00
t . Fatalf ( "unexpected build arguments: %q" , buildArgs )
2026-09-23 15:03:20 +00:00
}
2026-10-07 23:45:27 +00:00
if relative , err := filepath . Rel ( layout . repo , modfile ) ; err != nil || relative == "." || ( ! strings . HasPrefix ( relative , ".." + string ( filepath . Separator ) ) && relative != ".." ) {
t . Fatalf ( "alternate module file %q is not outside repository %q: relative=%q error=%v" , modfile , layout . repo , relative , err )
}
if module := readText ( t , layout . moduleLog ) ; ! strings . Contains ( module , "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0" ) {
t . Fatalf ( "temporary module file does not retain selected ReleaseKit version: %q" , module )
}
if sum := readText ( t , layout . sumLog ) ; ! strings . Contains ( sum , "fixture checksum" ) {
t . Fatalf ( "temporary sum file did not copy source checksums: %q" , sum )
}
if sourceModule := readText ( t , filepath . Join ( layout . repo , "go.mod" ) ) ; ! strings . Contains ( sourceModule , "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0" ) {
t . Fatalf ( "source go.mod changed: %q" , sourceModule )
}
if sourceSum := readText ( t , filepath . Join ( layout . repo , "go.sum" ) ) ; sourceSum != "fixture checksum\n" {
t . Fatalf ( "source go.sum changed: %q" , sourceSum )
}
if _ , err := os . Stat ( modfile ) ; ! os . IsNotExist ( err ) {
t . Fatalf ( "temporary module file remains after wrapper: %v" , err )
}
if _ , err := os . Stat ( filepath . Join ( filepath . Dir ( modfile ) , "releasekit.sum" ) ) ; ! os . IsNotExist ( err ) {
t . Fatalf ( "temporary sum file remains after wrapper: %v" , err )
}
2026-10-07 14:50:41 +00:00
if info , err := os . Stat ( filepath . Join ( layout . repo , "bin" , "releasekit" ) ) ; err != nil || ! info . Mode ( ) . IsRegular ( ) {
t . Fatalf ( "ReleaseKit executable was not built in bin: %v" , err )
2026-09-23 15:03:20 +00:00
}
2026-10-07 13:58:07 +00:00
toolInvocation := readNULArguments ( t , layout . argsLog )
wantToolInvocation := append ( [ ] string { layout . repo } , args ... )
if ! reflect . DeepEqual ( toolInvocation , wantToolInvocation ) {
t . Fatalf ( "tool invocation=%q, want %q" , toolInvocation , wantToolInvocation )
2026-09-23 15:03:20 +00:00
}
} )
}
}
2026-10-07 13:58:07 +00:00
2026-10-07 23:45:27 +00:00
func TestDeliveryWorkflowUsesModuleSelectedReleaseKit ( t * testing . T ) {
data , err := os . ReadFile ( "../.woodpecker/release.yml" )
if err != nil {
t . Fatal ( err )
}
var workflow struct {
Matrix struct {
Include [ ] map [ string ] string ` yaml:"include" `
} ` yaml:"matrix" `
When [ ] struct {
Event string ` yaml:"event" `
Ref string ` yaml:"ref" `
} ` yaml:"when" `
Steps [ ] struct {
Name string ` yaml:"name" `
Image string ` yaml:"image" `
Environment map [ string ] struct {
FromSecret string ` yaml:"from_secret" `
} ` yaml:"environment" `
Commands [ ] string ` yaml:"commands" `
} ` yaml:"steps" `
}
if err := yaml . Unmarshal ( data , & workflow ) ; err != nil {
t . Fatal ( err )
}
if len ( workflow . Matrix . Include ) != 1 || workflow . Matrix . Include [ 0 ] [ "GO_IMAGE" ] != "golang:1.25.5" {
t . Fatalf ( "workflow Go image = %v, want existing Go 1.25.5 image" , workflow . Matrix . Include )
}
if len ( workflow . When ) != 1 || workflow . When [ 0 ] . Event != "tag" || workflow . When [ 0 ] . Ref != "refs/tags/v*" {
t . Fatalf ( "workflow trigger = %+v" , workflow . When )
}
if len ( workflow . Steps ) != 1 || workflow . Steps [ 0 ] . Name != "deliver" || workflow . Steps [ 0 ] . Image != "${GO_IMAGE}" {
t . Fatalf ( "workflow delivery step = %+v" , workflow . Steps )
}
if secret := workflow . Steps [ 0 ] . Environment [ "RELEASEKIT_GITEA_TOKEN" ] . FromSecret ; secret != "GITEA_RELEASE_TOKEN" {
t . Fatalf ( "delivery secret = %q, want existing GITEA_RELEASE_TOKEN" , secret )
}
if len ( workflow . Steps [ 0 ] . Commands ) != 1 {
t . Fatalf ( "workflow commands = %q, want one bootstrap/delivery command" , workflow . Steps [ 0 ] . Commands )
}
command := workflow . Steps [ 0 ] . Commands [ 0 ]
for _ , fragment := range [ ] string {
"git fetch origin \"refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}\"" ,
"git checkout --detach \"$${tag_commit}\"" ,
"git status --porcelain --untracked-files=all" ,
"mktemp -d \"$${CI_WORKSPACE%/*}/.releasekit-bootstrap.XXXXXX\"" ,
"trap 'rm -rf \"$${bootstrap_dir}\"' EXIT" ,
"cp go.mod \"$${bootstrap_dir}/go.mod\"" ,
"cp go.sum \"$${bootstrap_dir}/go.sum\"" ,
"export GOWORK=off" ,
"export GOTOOLCHAIN=local" ,
"releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit" ,
"go list -mod=mod -modfile=" ,
"-m \"$${releasekit_module}\"" ,
"go build -mod=mod -modfile=" ,
"-o \"$${bootstrap_dir}/releasekit\" \"$${releasekit_module}/cmd/releasekit\"" ,
"\"$${bootstrap_dir}/releasekit\" ci-deliver" ,
"--repo \"$${CI_WORKSPACE}\"" ,
"--commit \"$${CI_COMMIT_SHA}\"" ,
"\"$${CI_COMMIT_TAG}\"" ,
} {
if ! strings . Contains ( command , fragment ) {
t . Fatalf ( "workflow command is missing %q: %s" , fragment , command )
}
}
if strings . Index ( command , "git checkout --detach" ) > strings . Index ( command , "go list -mod=mod" ) || strings . Index ( command , "git status --porcelain" ) > strings . Index ( command , "go list -mod=mod" ) {
t . Fatalf ( "workflow resolves the tool before verifying the tagged source: %s" , command )
}
if strings . Contains ( command , "RELEASEKIT_VERSION" ) || strings . Contains ( command , "cmd/releasekit@" ) {
t . Fatalf ( "workflow retains a separate tool version: %s" , command )
}
goMod , err := os . ReadFile ( "../go.mod" )
if err != nil {
t . Fatal ( err )
}
if ! strings . Contains ( string ( goMod ) , "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0" ) {
t . Fatalf ( "go.mod does not retain selected supported ReleaseKit version: %s" , goMod )
}
}
2026-10-07 13:58:07 +00:00
func TestReleaseWrapperStopsWhenBootstrapFails ( t * testing . T ) {
layout := newReleaseTestLayout ( t )
cmd := exec . Command ( "bash" , layout . wrapper , "publish" , "v4.2.0" )
cmd . Dir = layout . caller
cmd . Env = layout . environment ( map [ string ] string {
"RELEASEKIT_ARGS_LOG" : layout . argsLog ,
"RELEASEKIT_BUILD_LOG" : layout . buildLog ,
2026-10-07 23:45:27 +00:00
"RELEASEKIT_LIST_LOG" : layout . listLog ,
"RELEASEKIT_MODULE_LOG" : layout . moduleLog ,
"RELEASEKIT_SUM_LOG" : layout . sumLog ,
2026-10-07 13:58:07 +00:00
"RELEASEKIT_BOOTSTRAP_STATUS" : "42" ,
} )
output , err := cmd . CombinedOutput ( )
if got := commandStatus ( err ) ; got != 42 {
t . Fatalf ( "exit=%d, want 42; output=%q, error=%v" , got , output , err )
}
if _ , err := os . Stat ( layout . argsLog ) ; ! os . IsNotExist ( err ) {
t . Fatalf ( "ReleaseKit executed after failed bootstrap; stat error=%v" , err )
}
2026-10-07 23:45:27 +00:00
if _ , err := os . Stat ( layout . buildLog ) ; ! os . IsNotExist ( err ) {
t . Fatalf ( "ReleaseKit build ran after module selection failed; stat error=%v" , err )
}
listInvocation := readNULArguments ( t , layout . listLog )
modfile := argumentValue ( listInvocation [ 3 : ] , "-modfile=" )
if modfile == "" {
t . Fatalf ( "module selection did not use temporary metadata: %q" , listInvocation )
}
if _ , err := os . Stat ( filepath . Dir ( modfile ) ) ; ! os . IsNotExist ( err ) {
t . Fatalf ( "temporary bootstrap directory remains after failed selection: %v" , err )
}
2026-10-07 13:58:07 +00:00
}
func TestReleaseKitProfileDeclaresProductAndDeliverySettings ( t * testing . T ) {
data , err := os . ReadFile ( "../releasekit.json" )
if err != nil {
t . Fatal ( err )
}
var raw map [ string ] json . RawMessage
if err := json . Unmarshal ( data , & raw ) ; err != nil {
t . Fatal ( err )
}
var deliveryFields map [ string ] json . RawMessage
if err := json . Unmarshal ( raw [ "delivery" ] , & deliveryFields ) ; err != nil {
t . Fatal ( err )
}
if _ , exists := deliveryFields [ "token" ] ; exists {
t . Fatal ( "profile must not contain a credential value" )
}
var profile struct {
2026-10-07 14:50:41 +00:00
Product string ` json:"product" `
Remote string ` json:"remote" `
Checks [ ] json . RawMessage ` json:"checks" `
2026-10-07 13:58:07 +00:00
Build struct {
MainPackage string ` json:"main_package" `
BinaryName string ` json:"binary_name" `
Targets [ ] string ` json:"targets" `
} ` json:"build" `
Delivery struct {
BaseURL string ` json:"base_url" `
Owner string ` json:"owner" `
Repository string ` json:"repository" `
TokenEnv string ` json:"token_env" `
} ` json:"delivery" `
}
if err := json . Unmarshal ( data , & profile ) ; err != nil {
t . Fatal ( err )
}
2026-10-07 14:50:41 +00:00
if len ( profile . Checks ) != 0 {
t . Fatal ( "profile must not define additional checks" )
}
2026-10-07 13:58:07 +00:00
if profile . Product != "Audita" || profile . Remote != "origin" {
t . Fatalf ( "product=%q remote=%q" , profile . Product , profile . Remote )
}
if profile . Build . MainPackage != "./cmd/audita" || profile . Build . BinaryName != "audita" {
t . Fatalf ( "build=%+v" , profile . Build )
}
if ! sameMembers ( profile . Build . Targets , [ ] string { "linux/amd64" , "linux/arm64" } ) {
t . Fatalf ( "build targets=%q" , profile . Build . Targets )
}
if profile . Delivery . BaseURL != "https://gitea.maximumdirect.net" || profile . Delivery . Owner != "eric" || profile . Delivery . Repository != "audita" || profile . Delivery . TokenEnv != "RELEASEKIT_GITEA_TOKEN" {
t . Fatalf ( "delivery=%+v" , profile . Delivery )
}
}
2026-10-07 14:44:36 +00:00
func TestReleaseKitTargetMembership ( t * testing . T ) {
want := [ ] string { "linux/amd64" , "linux/arm64" }
for _ , test := range [ ] struct {
name string
targets [ ] string
match bool
} {
{ "missing amd64" , [ ] string { "linux/arm64" , "linux/arm64" } , false } ,
{ "missing arm64" , [ ] string { "linux/amd64" , "linux/amd64" } , false } ,
{ "additional target" , [ ] string { "linux/amd64" , "linux/arm64" , "darwin/arm64" } , false } ,
{ "reordered" , [ ] string { "linux/arm64" , "linux/amd64" } , true } ,
{ "repeated" , [ ] string { "linux/amd64" , "linux/arm64" , "linux/amd64" } , true } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
if got := sameMembers ( test . targets , want ) ; got != test . match {
t . Fatalf ( "target membership for %q = %t, want %t" , test . targets , got , test . match )
}
} )
}
}
2026-10-07 13:58:07 +00:00
type releaseTestLayout struct {
2026-10-07 23:45:27 +00:00
repo string
caller string
wrapper string
argsLog string
buildLog string
listLog string
moduleLog string
sumLog string
path string
2026-10-07 13:58:07 +00:00
}
func newReleaseTestLayout ( t * testing . T ) releaseTestLayout {
t . Helper ( )
root := t . TempDir ( )
layout := releaseTestLayout {
2026-10-07 23:45:27 +00:00
repo : filepath . Join ( root , "repository" ) ,
caller : filepath . Join ( root , "caller" ) ,
argsLog : filepath . Join ( root , "tool-arguments" ) ,
buildLog : filepath . Join ( root , "build-arguments" ) ,
listLog : filepath . Join ( root , "list-arguments" ) ,
moduleLog : filepath . Join ( root , "temporary-go.mod" ) ,
sumLog : filepath . Join ( root , "temporary-go.sum" ) ,
path : filepath . Join ( root , "bin" ) ,
2026-10-07 13:58:07 +00:00
}
if err := os . MkdirAll ( filepath . Join ( layout . repo , "scripts" ) , 0755 ) ; err != nil {
t . Fatal ( err )
}
if err := os . MkdirAll ( layout . caller , 0755 ) ; err != nil {
t . Fatal ( err )
}
if err := os . MkdirAll ( layout . path , 0755 ) ; err != nil {
t . Fatal ( err )
}
wrapper , err := os . ReadFile ( "release.sh" )
if err != nil {
t . Fatal ( err )
}
layout . wrapper = filepath . Join ( layout . repo , "scripts" , "release.sh" )
writeReleaseTestFile ( t , layout . wrapper , wrapper , 0755 )
writeReleaseTestFile ( t , filepath . Join ( layout . path , "go" ) , [ ] byte ( goStub ) , 0755 )
2026-10-07 23:45:27 +00:00
writeReleaseTestFile ( t , filepath . Join ( layout . repo , "go.mod" ) , [ ] byte ( "module example.com/audita-fixture\n\ngo 1.25.5\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.1.0\n" ) , 0644 )
writeReleaseTestFile ( t , filepath . Join ( layout . repo , "go.sum" ) , [ ] byte ( "fixture checksum\n" ) , 0644 )
2026-10-07 13:58:07 +00:00
return layout
}
2026-10-07 23:45:27 +00:00
func readText ( t * testing . T , path string ) string {
t . Helper ( )
data , err := os . ReadFile ( path )
if err != nil {
t . Fatal ( err )
}
return string ( data )
}
func argumentValue ( arguments [ ] string , prefix string ) string {
for _ , argument := range arguments {
if strings . HasPrefix ( argument , prefix ) {
return strings . TrimPrefix ( argument , prefix )
}
}
return ""
}
2026-10-07 13:58:07 +00:00
func ( layout releaseTestLayout ) environment ( values map [ string ] string ) [ ] string {
filtered := make ( [ ] string , 0 , len ( os . Environ ( ) ) + len ( values ) + 1 )
for _ , value := range os . Environ ( ) {
key , _ , _ := strings . Cut ( value , "=" )
2026-10-07 14:50:41 +00:00
if _ , overridden := values [ key ] ; key == "PATH" || overridden {
2026-10-07 13:58:07 +00:00
continue
}
filtered = append ( filtered , value )
}
filtered = append ( filtered , "PATH=" + layout . path + string ( os . PathListSeparator ) + os . Getenv ( "PATH" ) )
for key , value := range values {
filtered = append ( filtered , key + "=" + value )
}
return filtered
}
func writeReleaseTestFile ( t * testing . T , path string , data [ ] byte , mode os . FileMode ) {
t . Helper ( )
if err := os . WriteFile ( path , data , mode ) ; err != nil {
t . Fatal ( err )
}
}
func readNULArguments ( t * testing . T , path string ) [ ] string {
t . Helper ( )
data , err := os . ReadFile ( path )
if err != nil {
t . Fatal ( err )
}
parts := strings . Split ( string ( data ) , "\x00" )
if len ( parts ) > 0 && parts [ len ( parts ) - 1 ] == "" {
parts = parts [ : len ( parts ) - 1 ]
}
return parts
}
func commandStatus ( err error ) int {
if err == nil {
return 0
}
if exitErr , ok := err . ( * exec . ExitError ) ; ok {
return exitErr . ExitCode ( )
}
return - 1
}
func sameMembers ( got , want [ ] string ) bool {
2026-10-07 14:44:36 +00:00
gotSet := make ( map [ string ] struct { } , len ( got ) )
2026-10-07 13:58:07 +00:00
for _ , value := range got {
2026-10-07 14:44:36 +00:00
gotSet [ value ] = struct { } { }
}
wantSet := make ( map [ string ] struct { } , len ( want ) )
for _ , value := range want {
wantSet [ value ] = struct { } { }
2026-10-07 13:58:07 +00:00
}
2026-10-07 14:44:36 +00:00
return maps . Equal ( gotSet , wantSet )
2026-10-07 13:58:07 +00:00
}