Add build version reporting
This commit is contained in:
12
docs/cli.md
12
docs/cli.md
@@ -10,6 +10,7 @@ defined in [Operations](operations.md).
|
|||||||
|
|
||||||
~~~
|
~~~
|
||||||
notarius help
|
notarius help
|
||||||
|
notarius --version
|
||||||
notarius run <pipeline-id> --input path/to/source.json [--json] [flags]
|
notarius run <pipeline-id> --input path/to/source.json [--json] [flags]
|
||||||
notarius config validate [--config path/to/config.yml] [--pipeline pipeline-id] [--only lane-a,lane-b]
|
notarius config validate [--config path/to/config.yml] [--pipeline pipeline-id] [--only lane-a,lane-b]
|
||||||
notarius pipelines list [--config path/to/config.yml] [--json]
|
notarius pipelines list [--config path/to/config.yml] [--json]
|
||||||
@@ -18,6 +19,17 @@ notarius pipelines list [--config path/to/config.yml] [--json]
|
|||||||
Running Notarius without arguments, or with **help**, **--help**, or **-h**,
|
Running Notarius without arguments, or with **help**, **--help**, or **-h**,
|
||||||
writes the command summary to standard output and exits with status 0.
|
writes the command summary to standard output and exits with status 0.
|
||||||
|
|
||||||
|
`notarius --version` is valid only as the sole root argument. It writes exactly
|
||||||
|
`notarius <version>` followed by a newline to standard output and exits with
|
||||||
|
status 0. A tagged `go install` build can report its main-module stable tag,
|
||||||
|
and controlled builds can inject a stable tag at link time through
|
||||||
|
`gitea.maximumdirect.net/eric/notarius/internal/buildinfo.Override`; an ordinary
|
||||||
|
unversioned checkout reports `development`. Invalid injected version content is
|
||||||
|
a runtime error with exit status 1, while extra `--version` arguments are a
|
||||||
|
syntax error with exit status 2. This diagnostic does not replace the
|
||||||
|
[run-result](integrations/run-result.md) or artifact contracts for downstream
|
||||||
|
compatibility decisions.
|
||||||
|
|
||||||
## run
|
## run
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ preparation, and runner mechanics after their inputs are supplied.
|
|||||||
|
|
||||||
## Dispatch And Configuration Handoff
|
## Dispatch And Configuration Handoff
|
||||||
|
|
||||||
The root dispatcher handles help, configuration validation, pipeline listing,
|
The root dispatcher handles help, version reporting, configuration validation,
|
||||||
and a pipeline run. It normalizes injectable options before dispatch so that a
|
pipeline listing, and a pipeline run. Version reporting resolves build
|
||||||
missing production dependency fails as a command error rather than reaching
|
information through `internal/buildinfo` before production composition, so the
|
||||||
execution.
|
diagnostic remains available without configuration or runtime collaborators.
|
||||||
|
The public syntax, streams, exit classes, and version semantics are defined by
|
||||||
|
the [CLI reference](../cli.md). Other root commands normalize injectable
|
||||||
|
options before dispatch so that a missing production dependency fails as a
|
||||||
|
command error rather than reaching execution.
|
||||||
|
|
||||||
Commands that need configuration use one shared loader. The CLI discovers the
|
Commands that need configuration use one shared loader. The CLI discovers the
|
||||||
file, parses it through **internal/core/config**, starts from defaults, applies
|
file, parses it through **internal/core/config**, starts from defaults, applies
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ operation.
|
|||||||
- Tag CI validates only. Pre-publication local guards remain mandatory because
|
- Tag CI validates only. Pre-publication local guards remain mandatory because
|
||||||
tag CI cannot prevent an already-pushed tag.
|
tag CI cannot prevent an already-pushed tag.
|
||||||
|
|
||||||
## Stage 1: Add Build Version Resolution And `--version`
|
## Stage 1: Add Build Version Resolution And `--version` ✅
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
36
internal/buildinfo/buildinfo.go
Normal file
36
internal/buildinfo/buildinfo.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Package buildinfo resolves the product version embedded in a Notarius build.
|
||||||
|
package buildinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stableVersion = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
|
||||||
|
|
||||||
|
// Override is set at link time for controlled builds.
|
||||||
|
var Override string
|
||||||
|
|
||||||
|
// Version returns the release version embedded in the build, or development
|
||||||
|
// when the build does not carry a stable release tag.
|
||||||
|
func Version() (string, error) {
|
||||||
|
buildVersion := ""
|
||||||
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
buildVersion = info.Main.Version
|
||||||
|
}
|
||||||
|
return resolve(Override, buildVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve(override, buildVersion string) (string, error) {
|
||||||
|
if override != "" {
|
||||||
|
if !stableVersion.MatchString(override) {
|
||||||
|
return "", fmt.Errorf("build version override is not a stable release tag")
|
||||||
|
}
|
||||||
|
return override, nil
|
||||||
|
}
|
||||||
|
if stableVersion.MatchString(buildVersion) {
|
||||||
|
return buildVersion, nil
|
||||||
|
}
|
||||||
|
return "development", nil
|
||||||
|
}
|
||||||
45
internal/buildinfo/buildinfo_test.go
Normal file
45
internal/buildinfo/buildinfo_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package buildinfo
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestResolve(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
override string
|
||||||
|
buildVersion string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "stable main module version", buildVersion: "v1.2.3", want: "v1.2.3"},
|
||||||
|
{name: "zero version", buildVersion: "v0.0.0", want: "v0.0.0"},
|
||||||
|
{name: "override takes precedence", override: "v2.3.4", buildVersion: "v1.2.3", want: "v2.3.4"},
|
||||||
|
{name: "invalid override", override: "version", buildVersion: "v1.2.3", wantErr: true},
|
||||||
|
{name: "override with whitespace", override: " v1.2.3", wantErr: true},
|
||||||
|
{name: "leading zero major", buildVersion: "v01.2.3", want: "development"},
|
||||||
|
{name: "leading zero minor", buildVersion: "v1.02.3", want: "development"},
|
||||||
|
{name: "leading zero patch", buildVersion: "v1.2.03", want: "development"},
|
||||||
|
{name: "build version with whitespace", buildVersion: "v1.2.3 ", want: "development"},
|
||||||
|
{name: "prerelease", buildVersion: "v1.2.3-rc.1", want: "development"},
|
||||||
|
{name: "build suffix", buildVersion: "v1.2.3+build.1", want: "development"},
|
||||||
|
{name: "pseudo version", buildVersion: "v0.0.0-20260102030405-abcdef123456", want: "development"},
|
||||||
|
{name: "development build", buildVersion: "(devel)", want: "development"},
|
||||||
|
{name: "missing build information", want: "development"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := resolve(tt.override, tt.buildVersion)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("resolve() error = nil, want error")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("resolve() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/buildinfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCommandHelpSpellingsWriteUsageToStdout(t *testing.T) {
|
func TestCommandHelpSpellingsWriteUsageToStdout(t *testing.T) {
|
||||||
@@ -38,6 +40,7 @@ func TestCommandSyntaxErrorsUseStderrAndExitTwo(t *testing.T) {
|
|||||||
{name: "unknown pipelines subcommand", args: []string{"pipelines", "unknown"}, want: "unknown pipelines subcommand"},
|
{name: "unknown pipelines subcommand", args: []string{"pipelines", "unknown"}, want: "unknown pipelines subcommand"},
|
||||||
{name: "malformed run flag", args: []string{"run", "demo", "--chunk_cache", "invalid"}, want: "not supported"},
|
{name: "malformed run flag", args: []string{"run", "demo", "--chunk_cache", "invalid"}, want: "not supported"},
|
||||||
{name: "unknown flag", args: []string{"config", "validate", "--unknown"}, want: "flag provided but not defined"},
|
{name: "unknown flag", args: []string{"config", "validate", "--unknown"}, want: "flag provided but not defined"},
|
||||||
|
{name: "version arguments", args: []string{"--version", "extra"}, want: "--version does not accept arguments"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -50,6 +53,33 @@ func TestCommandSyntaxErrorsUseStderrAndExitTwo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandVersionOutput(t *testing.T) {
|
||||||
|
previous := buildinfo.Override
|
||||||
|
t.Cleanup(func() { buildinfo.Override = previous })
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
override string
|
||||||
|
wantCode int
|
||||||
|
wantStdout string
|
||||||
|
wantStderr string
|
||||||
|
}{
|
||||||
|
{name: "development", wantStdout: "notarius development\n"},
|
||||||
|
{name: "release override", override: "v1.2.3", wantStdout: "notarius v1.2.3\n"},
|
||||||
|
{name: "invalid override", override: "release", wantCode: 1, wantStderr: "notarius: build version override is not a stable release tag\n"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
buildinfo.Override = tt.override
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := RunWithOptions([]string{"--version"}, &stdout, &stderr, Options{})
|
||||||
|
if code != tt.wantCode || stdout.String() != tt.wantStdout || stderr.String() != tt.wantStderr {
|
||||||
|
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigDiscoveryPrefersExplicitPathThenEnvironment(t *testing.T) {
|
func TestConfigDiscoveryPrefersExplicitPathThenEnvironment(t *testing.T) {
|
||||||
explicit := writeCommandConfig(t, "explicit", "alpha")
|
explicit := writeCommandConfig(t, "explicit", "alpha")
|
||||||
environment := writeCommandConfig(t, "environment", "beta")
|
environment := writeCommandConfig(t, "environment", "beta")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/buildinfo"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/debugbundle"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/debugbundle"
|
||||||
@@ -31,6 +32,7 @@ import (
|
|||||||
const defaultConfigPath = "/usr/local/etc/notarius/config.yml"
|
const defaultConfigPath = "/usr/local/etc/notarius/config.yml"
|
||||||
const usage = `Usage:
|
const usage = `Usage:
|
||||||
notarius help
|
notarius help
|
||||||
|
notarius --version
|
||||||
notarius run <pipeline-id> --input path/to/source.json [--json] [flags]
|
notarius run <pipeline-id> --input path/to/source.json [--json] [flags]
|
||||||
notarius config validate --config path/to/config.yml [--pipeline pipeline-id] [--only lane-a,lane-b]
|
notarius config validate --config path/to/config.yml [--pipeline pipeline-id] [--only lane-a,lane-b]
|
||||||
notarius pipelines list --config path/to/config.yml [--json]
|
notarius pipelines list --config path/to/config.yml [--json]
|
||||||
@@ -62,6 +64,20 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RunWithOptions(args []string, stdout, stderr io.Writer, opts Options) int {
|
func RunWithOptions(args []string, stdout, stderr io.Writer, opts Options) int {
|
||||||
|
if len(args) > 0 && args[0] == "--version" {
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Fprintln(stderr, "notarius: --version does not accept arguments")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
version, err := buildinfo.Version()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stdout, "notarius %s\n", version)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
opts, err = normalizeOptions(opts)
|
opts, err = normalizeOptions(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user