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
|
boundary. This plan translates that target into bounded implementation stages
|
||||||
suitable for one `gpt-5.6-terra` coding prompt apiece.
|
suitable for one `gpt-5.6-terra` coding prompt apiece.
|
||||||
|
|
||||||
All stages are pending and must be implemented in numeric order. Each stage
|
All five stages were completed in numeric order. This document is retained as
|
||||||
must leave its owned behavior correct, tested at an appropriate stable
|
implementation history; current release behavior is owned by the checked-in
|
||||||
boundary, and ready for the next stage. This plan supersedes the completed
|
scripts and canonical release documentation. This plan superseded the completed
|
||||||
pipeline-configuration implementation plan that previously occupied this
|
pipeline-configuration implementation plan that previously occupied this path;
|
||||||
path; that feature's implemented behavior is now owned by current canonical
|
that feature's implemented behavior is now owned by current canonical
|
||||||
documentation and code.
|
documentation and code.
|
||||||
|
|
||||||
## Settled Implementation Decisions
|
## Settled Implementation Decisions
|
||||||
|
|||||||
@@ -11,6 +11,45 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestAssetBuilderRejectsUnsafeDestinations(t *testing.T) {
|
||||||
repoRoot := releaseCheckRepoRoot(t)
|
repoRoot := releaseCheckRepoRoot(t)
|
||||||
builder := filepath.Join(repoRoot, "scripts", "build-release-assets.sh")
|
builder := filepath.Join(repoRoot, "scripts", "build-release-assets.sh")
|
||||||
@@ -62,10 +101,15 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) {
|
|||||||
binDir := t.TempDir()
|
binDir := t.TempDir()
|
||||||
writeFakeGo(t, filepath.Join(binDir, "go"))
|
writeFakeGo(t, filepath.Join(binDir, "go"))
|
||||||
outputDir := filepath.Join(t.TempDir(), "assets")
|
outputDir := filepath.Join(t.TempDir(), "assets")
|
||||||
|
goLog := filepath.Join(t.TempDir(), "go.log")
|
||||||
|
|
||||||
command := exec.Command("sh", builder, "v1.2.3", outputDir)
|
command := exec.Command("sh", builder, "v1.2.3", outputDir)
|
||||||
command.Dir = t.TempDir()
|
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()
|
output, err := command.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("asset builder error = %v\n%s", err, output)
|
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" {
|
if got := versionOutput.String(); got != "narratio v1.2.3\n" {
|
||||||
t.Fatalf("embedded version output = %q", got)
|
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) {
|
func TestCandidateCheckerRejectsMalformedVersionAndReleaseNotes(t *testing.T) {
|
||||||
@@ -233,6 +286,10 @@ func writeFakeGo(t *testing.T, path string) {
|
|||||||
const fake = `#!/bin/sh
|
const fake = `#!/bin/sh
|
||||||
set -eu
|
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
|
if [ "$1" = env ]; then
|
||||||
case $2 in
|
case $2 in
|
||||||
GOOS) printf '%s\n' linux ;;
|
GOOS) printf '%s\n' linux ;;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P)
|
|||||||
|
|
||||||
tool_name=build-release-assets
|
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
|
if [ "$#" -ne 2 ]; then
|
||||||
narratio_release_fail "$tool_name" 'usage: scripts/build-release-assets.sh VERSION OUTPUT_DIR'
|
narratio_release_fail "$tool_name" 'usage: scripts/build-release-assets.sh VERSION OUTPUT_DIR'
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -11,32 +11,12 @@ narratio_release_fail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
narratio_release_validate_version() {
|
narratio_release_validate_version() {
|
||||||
version=$1
|
case $1 in
|
||||||
case $version in
|
*'
|
||||||
v*.*.*) ;;
|
'*) return 1 ;;
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
esac
|
||||||
components=${version#v}
|
printf '%s\n' "$1" |
|
||||||
if [ "$components" = "$version" ]; then
|
grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
narratio_release_repo_root() (
|
narratio_release_repo_root() (
|
||||||
|
|||||||
Reference in New Issue
Block a user