Harden release validation

This commit is contained in:
2026-08-30 20:36:23 +00:00
parent f8fa0a2623
commit 98139f7e8b
4 changed files with 73 additions and 31 deletions

View File

@@ -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

View File

@@ -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 ;;

View File

@@ -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

View File

@@ -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() (