Harden release validation
This commit is contained in:
@@ -8,11 +8,11 @@ accepted user intent, policy choices, required outcome, and responsibility
|
||||
boundary. This plan translates that target into bounded implementation stages
|
||||
suitable for one `gpt-5.6-terra` coding prompt apiece.
|
||||
|
||||
All stages are pending and must be implemented in numeric order. Each stage
|
||||
must leave its owned behavior correct, tested at an appropriate stable
|
||||
boundary, and ready for the next stage. This plan supersedes the completed
|
||||
pipeline-configuration implementation plan that previously occupied this
|
||||
path; that feature's implemented behavior is now owned by current canonical
|
||||
All five stages were completed in numeric order. This document is retained as
|
||||
implementation history; current release behavior is owned by the checked-in
|
||||
scripts and canonical release documentation. This plan superseded the completed
|
||||
pipeline-configuration implementation plan that previously occupied this path;
|
||||
that feature's implemented behavior is now owned by current canonical
|
||||
documentation and code.
|
||||
|
||||
## Settled Implementation Decisions
|
||||
|
||||
@@ -11,6 +11,45 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReleaseVersionValidationIsExact(t *testing.T) {
|
||||
repoRoot := releaseCheckRepoRoot(t)
|
||||
library := filepath.Join(repoRoot, "scripts", "release-lib.sh")
|
||||
workingDirectory := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(workingDirectory, "2"), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
version string
|
||||
valid bool
|
||||
}{
|
||||
{version: "v0.0.0", valid: true},
|
||||
{version: "v1.2.3", valid: true},
|
||||
{version: "v10.200.3000", valid: true},
|
||||
{version: "1.2.3"},
|
||||
{version: "v01.2.3"},
|
||||
{version: "v1.02.3"},
|
||||
{version: "v1.2.03"},
|
||||
{version: "v1.2.3."},
|
||||
{version: "v1.2.3-rc.1"},
|
||||
{version: "v1.2.3+build"},
|
||||
{version: "v1.*.3"},
|
||||
{version: "v1.2.3\nv4.5.6"},
|
||||
} {
|
||||
t.Run(test.version, func(t *testing.T) {
|
||||
command := exec.Command("sh", "-c", `. "$1"; narratio_release_validate_version "$2"`, "release-version-test", library, test.version)
|
||||
command.Dir = workingDirectory
|
||||
err := command.Run()
|
||||
if test.valid && err != nil {
|
||||
t.Fatalf("valid version %q rejected: %v", test.version, err)
|
||||
}
|
||||
if !test.valid && err == nil {
|
||||
t.Fatalf("invalid version %q accepted", test.version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetBuilderRejectsUnsafeDestinations(t *testing.T) {
|
||||
repoRoot := releaseCheckRepoRoot(t)
|
||||
builder := filepath.Join(repoRoot, "scripts", "build-release-assets.sh")
|
||||
@@ -62,10 +101,15 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
writeFakeGo(t, filepath.Join(binDir, "go"))
|
||||
outputDir := filepath.Join(t.TempDir(), "assets")
|
||||
goLog := filepath.Join(t.TempDir(), "go.log")
|
||||
|
||||
command := exec.Command("sh", builder, "v1.2.3", outputDir)
|
||||
command.Dir = t.TempDir()
|
||||
command.Env = append(os.Environ(), "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
command.Env = append(os.Environ(),
|
||||
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
||||
"GOWORK=/caller/controlled/go.work",
|
||||
"NARRATIO_RELEASE_ASSET_GO_LOG="+goLog,
|
||||
)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("asset builder error = %v\n%s", err, output)
|
||||
@@ -99,6 +143,15 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) {
|
||||
if got := versionOutput.String(); got != "narratio v1.2.3\n" {
|
||||
t.Fatalf("embedded version output = %q", got)
|
||||
}
|
||||
goCommands, err := os.ReadFile(goLog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(goCommands)), "\n") {
|
||||
if !strings.HasPrefix(line, "off ") {
|
||||
t.Fatalf("asset builder Go command did not receive GOWORK=off: %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateCheckerRejectsMalformedVersionAndReleaseNotes(t *testing.T) {
|
||||
@@ -233,6 +286,10 @@ func writeFakeGo(t *testing.T, path string) {
|
||||
const fake = `#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -n "${NARRATIO_RELEASE_ASSET_GO_LOG:-}" ]; then
|
||||
printf '%s %s\n' "${GOWORK:-}" "$*" >> "$NARRATIO_RELEASE_ASSET_GO_LOG"
|
||||
fi
|
||||
|
||||
if [ "$1" = env ]; then
|
||||
case $2 in
|
||||
GOOS) printf '%s\n' linux ;;
|
||||
|
||||
@@ -7,6 +7,11 @@ script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P)
|
||||
|
||||
tool_name=build-release-assets
|
||||
|
||||
# Release builds always use the tagged module in this repository. Own this
|
||||
# setting here so direct callers and CI cannot supply an ambient Go workspace.
|
||||
GOWORK=off
|
||||
export GOWORK
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
narratio_release_fail "$tool_name" 'usage: scripts/build-release-assets.sh VERSION OUTPUT_DIR'
|
||||
fi
|
||||
|
||||
@@ -11,32 +11,12 @@ narratio_release_fail() {
|
||||
}
|
||||
|
||||
narratio_release_validate_version() {
|
||||
version=$1
|
||||
case $version in
|
||||
v*.*.*) ;;
|
||||
*) return 1 ;;
|
||||
case $1 in
|
||||
*'
|
||||
'*) return 1 ;;
|
||||
esac
|
||||
components=${version#v}
|
||||
if [ "$components" = "$version" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
previous_ifs=$IFS
|
||||
IFS=.
|
||||
set -- $components
|
||||
IFS=$previous_ifs
|
||||
if [ "$#" -ne 3 ]; then
|
||||
return 1
|
||||
fi
|
||||
for component in "$@"; do
|
||||
case $component in
|
||||
0 | [1-9]*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
case $component in
|
||||
*[!0-9]*) return 1 ;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "$1" |
|
||||
grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
|
||||
}
|
||||
|
||||
narratio_release_repo_root() (
|
||||
|
||||
Reference in New Issue
Block a user