Enable race testing in validation
This commit is contained in:
24
check.go
24
check.go
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH.
|
// ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH.
|
||||||
@@ -27,18 +28,34 @@ type ReleaseRequest struct {
|
|||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
Remote string
|
Remote string
|
||||||
// AdditionalChecks optionally runs product checks after the standard suite.
|
// Validation selects optional checks run against the captured candidate.
|
||||||
|
Validation ValidationSpec
|
||||||
|
// AdditionalChecks optionally runs product checks after shared validation.
|
||||||
AdditionalChecks func(context.Context, Candidate) error
|
AdditionalChecks func(context.Context, Candidate) error
|
||||||
Build *BuildSpec // optional selected-target build after validation.
|
Build *BuildSpec // optional selected-target build after validation.
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidationSpec selects optional shared validation checks. Its zero value
|
||||||
|
// preserves the current validation behavior.
|
||||||
|
type ValidationSpec struct {
|
||||||
|
Go GoValidationSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoValidationSpec selects checks performed by the Go toolchain.
|
||||||
|
type GoValidationSpec struct {
|
||||||
|
// Race runs the one standard Go test suite with the race detector enabled.
|
||||||
|
Race bool
|
||||||
|
}
|
||||||
|
|
||||||
// Check validates one local, committed release candidate. It requires a clean
|
// Check validates one local, committed release candidate. It requires a clean
|
||||||
// worktree and index on main, a committed note matching Product and Version,
|
// worktree and index on main, a committed note matching Product and Version,
|
||||||
// and an unused local tag. It always runs the offline Go test suite with the
|
// and an unused local tag. It always runs the offline Go test suite with the
|
||||||
// available compatible local Go toolchain and Go host target, then calls
|
// available compatible local Go toolchain and Go host target, then calls
|
||||||
// AdditionalChecks when supplied. If Build selects targets, Check builds them
|
// AdditionalChecks when supplied. Validation.Go.Race runs that one suite with
|
||||||
// after validation in temporary storage outside the worktree, then rechecks
|
// the race detector; the zero value preserves the ordinary native suite. If
|
||||||
|
// Build selects targets, Check builds them after validation in temporary
|
||||||
|
// storage outside the worktree, then rechecks
|
||||||
// branch, HEAD, worktree, index, and local tag state. Success establishes local
|
// branch, HEAD, worktree, index, and local tag state. Success establishes local
|
||||||
// candidate evidence only; it does not establish remote availability.
|
// candidate evidence only; it does not establish remote availability.
|
||||||
func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
||||||
@@ -58,6 +75,7 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
|||||||
RepositoryDir: request.RepositoryDir,
|
RepositoryDir: request.RepositoryDir,
|
||||||
Product: request.Product,
|
Product: request.Product,
|
||||||
Version: request.Version,
|
Version: request.Version,
|
||||||
|
Validation: validation.Settings{Race: request.Validation.Go.Race},
|
||||||
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
||||||
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
||||||
Output: request.Output,
|
Output: request.Output,
|
||||||
|
|||||||
@@ -112,14 +112,25 @@ type CheckCommand struct {
|
|||||||
Env map[string]string `json:"env,omitempty"`
|
Env map[string]string `json:"env,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings contains the product, remote, and check commands read from a
|
// Settings contains the product, remote, shared validation selections, and
|
||||||
// releasekit.json profile.
|
// check commands read from a releasekit.json profile.
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
Remote string `json:"remote,omitempty"`
|
Remote string `json:"remote,omitempty"`
|
||||||
Checks []CheckCommand `json:"checks,omitempty"`
|
Checks []CheckCommand `json:"checks,omitempty"`
|
||||||
Build *BuildSettings `json:"build,omitempty"`
|
Validation *ValidationSettings `json:"validation,omitempty"`
|
||||||
Delivery *DeliverySettings `json:"delivery,omitempty"`
|
Build *BuildSettings `json:"build,omitempty"`
|
||||||
|
Delivery *DeliverySettings `json:"delivery,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationSettings selects shared candidate checks in a release profile.
|
||||||
|
type ValidationSettings struct {
|
||||||
|
Go GoValidationSettings `json:"go,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoValidationSettings selects checks performed by the Go toolchain.
|
||||||
|
type GoValidationSettings struct {
|
||||||
|
Race bool `json:"race,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeliverySettings selects the hosted Gitea release destination.
|
// DeliverySettings selects the hosted Gitea release destination.
|
||||||
|
|||||||
@@ -141,6 +141,22 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadSettingsReadsRaceValidation(t *testing.T) {
|
||||||
|
repoDir := t.TempDir()
|
||||||
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
|
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true}}}`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := LoadSettings(repoDir, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if settings.Validation == nil || !settings.Validation.Go.Race {
|
||||||
|
t.Fatalf("validation settings = %#v, want Go race enabled", settings.Validation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExampleProfileLoads(t *testing.T) {
|
func TestExampleProfileLoads(t *testing.T) {
|
||||||
repoDir, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
|
repoDir, err := filepath.Abs(filepath.Join("..", "..", "..", ".."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func candidateRequest(invocation cli.Invocation, settings cli.Settings, output i
|
|||||||
Product: settings.Product,
|
Product: settings.Product,
|
||||||
Version: invocation.Version,
|
Version: invocation.Version,
|
||||||
Remote: settings.Remote,
|
Remote: settings.Remote,
|
||||||
|
Validation: validationSpec(settings.Validation),
|
||||||
Build: configuredBuildSpec(settings.Build),
|
Build: configuredBuildSpec(settings.Build),
|
||||||
Output: output,
|
Output: output,
|
||||||
}
|
}
|
||||||
@@ -154,6 +155,15 @@ func candidateRequest(invocation cli.Invocation, settings cli.Settings, output i
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec {
|
||||||
|
if settings == nil {
|
||||||
|
return releasekit.ValidationSpec{}
|
||||||
|
}
|
||||||
|
return releasekit.ValidationSpec{
|
||||||
|
Go: releasekit.GoValidationSpec{Race: settings.Go.Race},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func candidateFailureText(err error) string {
|
func candidateFailureText(err error) string {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ func TestUnknownCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
||||||
|
raceSelected := racePrerequisitesAvailable()
|
||||||
repository := newCLIRepository(t)
|
repository := newCLIRepository(t)
|
||||||
profilePath := filepath.Join(t.TempDir(), "profile with spaces.json")
|
profilePath := filepath.Join(t.TempDir(), "profile with spaces.json")
|
||||||
orderPath := filepath.Join(t.TempDir(), "check-order")
|
orderPath := filepath.Join(t.TempDir(), "check-order")
|
||||||
@@ -73,6 +74,9 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
|||||||
Remote: "origin",
|
Remote: "origin",
|
||||||
Checks: []cli.CheckCommand{check("first"), check("second")},
|
Checks: []cli.CheckCommand{check("first"), check("second")},
|
||||||
}
|
}
|
||||||
|
if raceSelected {
|
||||||
|
settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}}
|
||||||
|
}
|
||||||
contents, err := json.Marshal(settings)
|
contents, err := json.Marshal(settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -82,6 +86,11 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
|
raceMarker := ""
|
||||||
|
if raceSelected {
|
||||||
|
raceMarker = filepath.Join(t.TempDir(), "race-enabled")
|
||||||
|
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
||||||
|
}
|
||||||
args := []string{"check", "--repo", repository, "--config", profilePath, "v1.2.3"}
|
args := []string{"check", "--repo", repository, "--config", profilePath, "v1.2.3"}
|
||||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
||||||
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
||||||
@@ -96,6 +105,23 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
|||||||
if want := fmt.Sprintf("local candidate v1.2.3 at %s is valid\n", commit); stdout.String() != want {
|
if want := fmt.Sprintf("local candidate v1.2.3 at %s is valid\n", commit); stdout.String() != want {
|
||||||
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
|
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
|
||||||
}
|
}
|
||||||
|
if raceSelected {
|
||||||
|
if _, err := os.Stat(raceMarker); err != nil {
|
||||||
|
t.Fatalf("race-enabled profile did not run: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCandidateRequestMapsRaceValidation(t *testing.T) {
|
||||||
|
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
||||||
|
Product: "Example Product",
|
||||||
|
Validation: &cli.ValidationSettings{
|
||||||
|
Go: cli.GoValidationSettings{Race: true},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if !request.Validation.Go.Race {
|
||||||
|
t.Fatalf("candidate request validation = %#v, want Go race enabled", request.Validation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
||||||
@@ -165,6 +191,7 @@ func TestCandidateFailurePresentationRetainsCallbackContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
|
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
|
||||||
|
raceSelected := racePrerequisitesAvailable()
|
||||||
repository := newCLIRepository(t)
|
repository := newCLIRepository(t)
|
||||||
writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n")
|
writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n")
|
||||||
writeRepositoryFile(t, repository, "scripts/check-release-note.sh", `#!/bin/sh
|
writeRepositoryFile(t, repository, "scripts/check-release-note.sh", `#!/bin/sh
|
||||||
@@ -189,6 +216,9 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
|||||||
Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog},
|
Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
if raceSelected {
|
||||||
|
settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}}
|
||||||
|
}
|
||||||
contents, err := json.Marshal(settings)
|
contents, err := json.Marshal(settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -204,6 +234,11 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
|||||||
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
|
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
|
raceMarker := ""
|
||||||
|
if raceSelected {
|
||||||
|
raceMarker = filepath.Join(t.TempDir(), "race-enabled")
|
||||||
|
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
||||||
|
}
|
||||||
args := []string{"publish", "--repo", repository, "v1.2.3"}
|
args := []string{"publish", "--repo", repository, "v1.2.3"}
|
||||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
||||||
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
||||||
@@ -226,6 +261,11 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
|||||||
if got, want := strings.Count(string(checkResult), "v1.2.3 "+commit), 1; got != want {
|
if got, want := strings.Count(string(checkResult), "v1.2.3 "+commit), 1; got != want {
|
||||||
t.Fatalf("product compatibility check ran %d times, want %d", got, want)
|
t.Fatalf("product compatibility check ran %d times, want %d", got, want)
|
||||||
}
|
}
|
||||||
|
if raceSelected {
|
||||||
|
if _, err := os.Stat(raceMarker); err != nil {
|
||||||
|
t.Fatalf("race-enabled profile did not run: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) {
|
func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) {
|
||||||
@@ -343,12 +383,52 @@ func newCLIRepository(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
write("go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n")
|
write("go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n")
|
||||||
write("candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
|
write("candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
|
||||||
|
write("race_candidate_test.go", `//go:build race
|
||||||
|
|
||||||
|
package candidate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRaceBuildSelection(t *testing.T) {
|
||||||
|
marker := os.Getenv("RELEASEKIT_RACE_MARKER")
|
||||||
|
if marker == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(marker, []byte("race enabled"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
write("docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nRelease note.\n")
|
write("docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nRelease note.\n")
|
||||||
cliGitRun(t, repository, "add", "--all")
|
cliGitRun(t, repository, "add", "--all")
|
||||||
cliGitRun(t, repository, "commit", "-m", "candidate")
|
cliGitRun(t, repository, "commit", "-m", "candidate")
|
||||||
return repository
|
return repository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func racePrerequisitesAvailable() bool {
|
||||||
|
supported := runtime.GOOS == "darwin" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") ||
|
||||||
|
(runtime.GOOS == "freebsd" || runtime.GOOS == "linux") && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le") ||
|
||||||
|
runtime.GOOS == "windows" && runtime.GOARCH == "amd64"
|
||||||
|
if !supported {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
||||||
|
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cc, err := exec.Command("go", "env", "CC").Output()
|
||||||
|
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
|
func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
|
||||||
repository := newCLIRepository(t)
|
repository := newCLIRepository(t)
|
||||||
state := &deliveryServerState{}
|
state := &deliveryServerState{}
|
||||||
|
|||||||
17
docs/cli.md
17
docs/cli.md
@@ -23,10 +23,10 @@ go run ./cmd/releasekit check [--repo DIR] [--config FILE] VERSION
|
|||||||
`VERSION` must use stable `vMAJOR.MINOR.PATCH` syntax. Options must precede the
|
`VERSION` must use stable `vMAJOR.MINOR.PATCH` syntax. Options must precede the
|
||||||
version. The repository defaults to the current directory, and the profile
|
version. The repository defaults to the current directory, and the profile
|
||||||
defaults to `<repository>/releasekit.json`. A successful command prints the
|
defaults to `<repository>/releasekit.json`. A successful command prints the
|
||||||
captured local commit and version. It checks the committed release note, local
|
captured local commit and version. It checks the committed release note and
|
||||||
Git state, runs the standard offline Go test suite, and then runs any configured
|
local Git state, runs the standard offline Go test suite (with `-race` when
|
||||||
additional checks in profile order. It does not inspect remote availability or
|
`validation.go.race` is true), and then runs any configured additional checks
|
||||||
publish a tag.
|
in profile order. It does not inspect remote availability or publish a tag.
|
||||||
|
|
||||||
`check` writes errors and diagnostics to standard error. It returns status 0
|
`check` writes errors and diagnostics to standard error. It returns status 0
|
||||||
after local validation, 1 after a candidate validation failure, and 2 for usage,
|
after local validation, 1 after a candidate validation failure, and 2 for usage,
|
||||||
@@ -40,10 +40,11 @@ Publish a source tag:
|
|||||||
go run ./cmd/releasekit publish [--repo DIR] [--config FILE] VERSION
|
go run ./cmd/releasekit publish [--repo DIR] [--config FILE] VERSION
|
||||||
```
|
```
|
||||||
|
|
||||||
`publish` reads the same product, remote, and additional-check settings as
|
`publish` reads the same product, remote, validation, and additional-check
|
||||||
`check`. It checks the authoritative remote and validates the candidate: the
|
settings as `check`. It checks the authoritative remote and validates the
|
||||||
standard offline Go test suite runs first, followed by configured checks in
|
candidate: the single standard offline Go test suite runs first, with `-race`
|
||||||
profile order. It then creates an annotated local tag and pushes only its exact
|
when `validation.go.race` is true, followed by configured checks in profile
|
||||||
|
order. It then creates an annotated local tag and pushes only its exact
|
||||||
tag object. It prints the confirmed version, candidate commit, and tag object
|
tag object. It prints the confirmed version, candidate commit, and tag object
|
||||||
on success. Validation
|
on success. Validation
|
||||||
progress and failed publication state, including the captured publication
|
progress and failed publication state, including the captured publication
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ The current profile fields are:
|
|||||||
| `product` | Yes | Product name used by release operations. |
|
| `product` | Yes | Product name used by release operations. |
|
||||||
| `remote` | No | Selected Git remote; defaults to `origin`. |
|
| `remote` | No | Selected Git remote; defaults to `origin`. |
|
||||||
| `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. |
|
| `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. |
|
||||||
|
| `validation` | No | Optional shared checks selected by ReleaseKit. |
|
||||||
| `build` | No | Optional main package, binary name, and selected `GOOS/GOARCH` target strings. |
|
| `build` | No | Optional main package, binary name, and selected `GOOS/GOARCH` target strings. |
|
||||||
| `delivery` | No | Optional Gitea `base_url`, `owner`, `repository`, and `token_env` settings for tagged-source delivery. |
|
| `delivery` | No | Optional Gitea `base_url`, `owner`, `repository`, and `token_env` settings for tagged-source delivery. |
|
||||||
|
|
||||||
@@ -25,6 +26,17 @@ Unknown fields are rejected so a setting cannot be silently ignored. The
|
|||||||
target requires the package and binary name; each target is written as one
|
target requires the package and binary name; each target is written as one
|
||||||
`GOOS/GOARCH` pair. An omitted or empty target list means no binary work.
|
`GOOS/GOARCH` pair. An omitted or empty target list means no binary work.
|
||||||
|
|
||||||
|
The optional `validation` object selects shared checks implemented by ReleaseKit.
|
||||||
|
`validation.go.race` enables the race detector for the single standard Go test
|
||||||
|
suite. When omitted or false, the ordinary native suite runs. For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"product": "Example",
|
||||||
|
"validation": {"go": {"race": true}}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
The `delivery` object contains `base_url`, `owner`, `repository`, and optional
|
The `delivery` object contains `base_url`, `owner`, `repository`, and optional
|
||||||
`token_env`. Its values are required when running `deliver`, but credentials are
|
`token_env`. Its values are required when running `deliver`, but credentials are
|
||||||
not required for `check`, `publish`, or `build`. `token_env` names the
|
not required for `check`, `publish`, or `build`. `token_env` names the
|
||||||
@@ -32,10 +44,11 @@ environment variable containing the Gitea token; it defaults to
|
|||||||
`RELEASEKIT_GITEA_TOKEN`. The CLI reads this variable only for `deliver` and
|
`RELEASEKIT_GITEA_TOKEN`. The CLI reads this variable only for `deliver` and
|
||||||
passes its value explicitly to the library operation.
|
passes its value explicitly to the library operation.
|
||||||
|
|
||||||
The `check` and `publish` commands always run
|
The `check` and `publish` commands run one
|
||||||
`go test -mod=readonly -count=1 ./...` with Go module and toolchain network
|
`go test -mod=readonly -count=1 ./...` suite with Go module and toolchain network
|
||||||
access disabled. They then run each configured `checks` command once, in order,
|
access disabled, adding `-race` when `validation.go.race` is true. They then run
|
||||||
as an additional product check. Selected-target builds run after both. Do not
|
each configured `checks` command once, in order, as an additional product
|
||||||
|
check. Selected-target builds run after the suite and product checks. Do not
|
||||||
repeat the standard suite in `checks`; doing so runs it twice. The `build`
|
repeat the standard suite in `checks`; doing so runs it twice. The `build`
|
||||||
command uses the selected build settings and writes binaries to `dist` unless
|
command uses the selected build settings and writes binaries to `dist` unless
|
||||||
`--output` selects another path.
|
`--output` selects another path.
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ result, err := releasekit.Check(ctx, releasekit.ReleaseRequest{
|
|||||||
RepositoryDir: "/work/project",
|
RepositoryDir: "/work/project",
|
||||||
Product: "Example",
|
Product: "Example",
|
||||||
Version: "v1.2.3",
|
Version: "v1.2.3",
|
||||||
|
Validation: releasekit.ValidationSpec{
|
||||||
|
Go: releasekit.GoValidationSpec{Race: true},
|
||||||
|
},
|
||||||
AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error {
|
AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error {
|
||||||
return runProjectChecks(ctx, candidate.RepositoryDir)
|
return runProjectChecks(ctx, candidate.RepositoryDir)
|
||||||
},
|
},
|
||||||
@@ -24,14 +27,17 @@ version must be stable `vMAJOR.MINOR.PATCH`. The committed
|
|||||||
product name and exact version as complete title elements. No local tag with
|
product name and exact version as complete title elements. No local tag with
|
||||||
that version may already exist.
|
that version may already exist.
|
||||||
|
|
||||||
ReleaseKit always runs `go test -mod=readonly -count=1 ./...` with the available
|
ReleaseKit runs one `go test -mod=readonly -count=1 ./...` suite with the
|
||||||
compatible local Go toolchain, then calls `AdditionalChecks` once when supplied.
|
available compatible local Go toolchain, then calls `AdditionalChecks` once
|
||||||
It does not download a different toolchain automatically. The standard suite
|
when supplied. Set `Validation.Go.Race` to run that suite with `-race`; the zero
|
||||||
uses the Go host target, disables workspace and module network access, and
|
value keeps the ordinary native suite. ReleaseKit does not download a different
|
||||||
ignores ambient `GOFLAGS`, experiment, and CPU feature overrides. Effective
|
toolchain automatically. If the race prerequisite is unavailable, the `go-test`
|
||||||
cache and native cgo/C-tool settings are preserved, including values from Go's
|
check fails rather than being skipped. The suite uses the Go host target,
|
||||||
persistent environment file. The check revalidates the branch, commit, and
|
disables workspace and module network access, and ignores ambient `GOFLAGS`,
|
||||||
cleanliness after validation.
|
experiment, and CPU feature overrides. Effective cache and native cgo/C-tool
|
||||||
|
settings are preserved, including values from Go's persistent environment
|
||||||
|
file. The check revalidates the branch, commit, and cleanliness after
|
||||||
|
validation.
|
||||||
|
|
||||||
A successful check is evidence about the local candidate only. It does not
|
A successful check is evidence about the local candidate only. It does not
|
||||||
check whether a remote branch or tag is available and does not create a tag.
|
check whether a remote branch or tag is available and does not create a tag.
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ result, err := releasekit.Publish(ctx, releasekit.ReleaseRequest{
|
|||||||
RepositoryDir: "/work/project",
|
RepositoryDir: "/work/project",
|
||||||
Product: "Example",
|
Product: "Example",
|
||||||
Version: "v1.2.3",
|
Version: "v1.2.3",
|
||||||
|
Validation: releasekit.ValidationSpec{
|
||||||
|
Go: releasekit.GoValidationSpec{Race: true},
|
||||||
|
},
|
||||||
AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error {
|
AdditionalChecks: func(ctx context.Context, candidate releasekit.Candidate) error {
|
||||||
return runProjectChecks(ctx, candidate.RepositoryDir)
|
return runProjectChecks(ctx, candidate.RepositoryDir)
|
||||||
},
|
},
|
||||||
@@ -20,12 +23,15 @@ fmt.Println(result.Version, result.CandidateCommit, result.TagObject)
|
|||||||
|
|
||||||
Publication captures a clean candidate on `main`, validates its committed
|
Publication captures a clean candidate on `main`, validates its committed
|
||||||
release note, and checks the selected remote before running candidate checks.
|
release note, and checks the selected remote before running candidate checks.
|
||||||
It always runs `go test -mod=readonly -count=1 ./...` with the available
|
It runs one `go test -mod=readonly -count=1 ./...` suite with the available
|
||||||
compatible local Go toolchain. It then calls `AdditionalChecks` once when
|
compatible local Go toolchain. Set `Validation.Go.Race` to run that suite with
|
||||||
supplied. The standard suite cannot be replaced; move product-specific behavior
|
`-race`; its zero value keeps the ordinary native suite. It then calls
|
||||||
into `AdditionalChecks` and remove repeated standard-suite commands from custom
|
`AdditionalChecks` once when supplied. The standard suite cannot be replaced;
|
||||||
checks. A failed suite or additional check prevents tag creation, and wrapped
|
move product-specific behavior into `AdditionalChecks` and remove repeated
|
||||||
errors preserve their causes.
|
standard-suite commands from custom checks. A failed suite or additional check
|
||||||
|
prevents tag creation, and wrapped errors preserve their causes.
|
||||||
|
If race prerequisites are unavailable, the selected suite fails as `go-test`
|
||||||
|
instead of silently running without the race detector.
|
||||||
|
|
||||||
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
||||||
and additional checks, before creating the tag. The operation then rechecks the
|
and additional checks, before creating the tag. The operation then rechecks the
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ these implemented packages:
|
|||||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
||||||
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||||
| `internal/validation` | Shared default Go validation, structured check failures, and lazy committed-file access for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
| `internal/validation` | Shared Go validation with optional race selection, structured check failures, and lazy committed-file access for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
||||||
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||||
@@ -78,7 +78,7 @@ not a separate release matrix:
|
|||||||
| Capability | Implementation owner | Existing evidence |
|
| Capability | Implementation owner | Existing evidence |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
||||||
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce` |
|
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestRunUsesOneSuiteWithSelectedRaceSetting`, `TestRunReportsRacePrerequisiteFailureWithoutFallback`, `TestRunExecutesRaceEnabledSuiteOnHost`, `TestCandidateRequestMapsRaceValidation`, `TestLoadSettingsReadsRaceValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce` |
|
||||||
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ post-validation build callback and local-state recheck. `internal/publish`
|
|||||||
adds its endpoint guards, tag creation, push, and recovery around that same
|
adds its endpoint guards, tag creation, push, and recovery around that same
|
||||||
candidate path.
|
candidate path.
|
||||||
|
|
||||||
The current executor runs the default offline Go suite. Its request carries the
|
The executor runs one offline Go suite, optionally with the race detector when
|
||||||
captured repository directory and commit, output writer, and internal
|
selected. Its request carries the captured repository directory and commit,
|
||||||
settings. `Request.ReadCandidateTree` lets a selected file check request
|
output writer, and internal settings. `Request.ReadCandidateTree` lets a
|
||||||
candidate contents; the default Go-only path does not enumerate the tree.
|
selected file check request candidate contents; the Go-only path does not
|
||||||
|
enumerate the tree.
|
||||||
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
||||||
lazily, and resolves destinations without consulting the worktree or
|
lazily, and resolves destinations without consulting the worktree or
|
||||||
neighboring files.
|
neighboring files.
|
||||||
|
|||||||
@@ -226,6 +226,11 @@ imports only lightweight `buildinfo`. Stages 12–14 implement this agreed appro
|
|||||||
|
|
||||||
## Stage 1: Introduce the specification with race selection
|
## Stage 1: Introduce the specification with race selection
|
||||||
|
|
||||||
|
**Completion:** Complete. `ReleaseRequest.Validation.Go.Race` and profile
|
||||||
|
`validation.go.race` select `-race` for the single native Go suite. Both
|
||||||
|
candidate operations share the selection, and race failures retain the `go-test`
|
||||||
|
identity without falling back to an ordinary suite.
|
||||||
|
|
||||||
**Outcome:** The first public/profile selection changes the one existing suite.
|
**Outcome:** The first public/profile selection changes the one existing suite.
|
||||||
|
|
||||||
1. Add `ReleaseRequest.Validation`, `ValidationSpec.Go`, and
|
1. Add `ReleaseRequest.Validation`, `ValidationSpec.Go`, and
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Validation API and profile model
|
# Validation API and profile model
|
||||||
|
|
||||||
Status: Shared validation foundation implemented; optional v0.2.0 capabilities
|
Status: Shared validation foundation and Go race selection implemented;
|
||||||
and their public/profile settings remain future work.
|
remaining v0.2.0 capabilities and their settings are future work.
|
||||||
|
|
||||||
This roadmap describes the shared model that enables the optional validation
|
This roadmap describes the shared model that enables the optional validation
|
||||||
capabilities in the [v0.2.0 roadmap](v0.2.0.md). It owns their common API,
|
capabilities in the [v0.2.0 roadmap](v0.2.0.md). It owns their common API,
|
||||||
@@ -18,9 +18,9 @@ Selected executable builds follow validation. The CLI maps profile `checks`
|
|||||||
commands to the additional-check callback.
|
commands to the additional-check callback.
|
||||||
|
|
||||||
This provides a common procedure and a product-specific extension point. The
|
This provides a common procedure and a product-specific extension point. The
|
||||||
request and profile currently have no structured selection for shared optional
|
request and profile now have a structured selection for race testing; full vet,
|
||||||
validation. Race testing, full vet, package compilation, formatting, Markdown,
|
package compilation, formatting, Markdown, module-distribution, and file-size
|
||||||
module-distribution, and file-size checks need a coherent home as they are added.
|
checks will use the same model as their capabilities are added.
|
||||||
|
|
||||||
The model should let consumers declare their choices once while ReleaseKit owns
|
The model should let consumers declare their choices once while ReleaseKit owns
|
||||||
execution and coordination. This follows [ADR 0002](../adr/0002-routine-releases.md):
|
execution and coordination. This follows [ADR 0002](../adr/0002-routine-releases.md):
|
||||||
@@ -124,11 +124,10 @@ failure-stopping behavior of the candidate workflow.
|
|||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
The [implementation plan](implementation.md) records the completed foundation:
|
The [implementation plan](implementation.md) records the completed foundation
|
||||||
shared execution, internal diagnostics, captured candidate-tree inspection and
|
and current rollout. The facade exposes `ReleaseRequest.Validation.Go.Race`, and
|
||||||
destination resolution, facade diagnostics, and consistent CLI presentation.
|
profiles accept `validation.go.race`; both select `-race` for the one base Go
|
||||||
The public `ReleaseRequest.Validation` field and profile `validation` object are
|
suite. Each remaining setting will be introduced with its first working
|
||||||
not yet available; each setting will be introduced with its first working
|
|
||||||
capability. Individual checkers, module-selected CI tooling, and the publication
|
capability. Individual checkers, module-selected CI tooling, and the publication
|
||||||
endpoint expectation remain future work in the [v0.2.0 roadmap](v0.2.0.md).
|
endpoint expectation remain future work in the [v0.2.0 roadmap](v0.2.0.md).
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Request struct {
|
|||||||
RepositoryDir string
|
RepositoryDir string
|
||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
|
Validation validation.Settings
|
||||||
AdditionalChecks func(context.Context, Candidate) error
|
AdditionalChecks func(context.Context, Candidate) error
|
||||||
AfterValidate func(context.Context, Candidate) error
|
AfterValidate func(context.Context, Candidate) error
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
@@ -95,13 +96,14 @@ func Capture(ctx context.Context, request Request) (Candidate, error) {
|
|||||||
return Candidate{RepositoryDir: repositoryDir, Commit: state.commit, Version: request.Version, Note: string(note)}, nil
|
return Candidate{RepositoryDir: repositoryDir, Commit: state.commit, Version: request.Version, Note: string(note)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate runs the standard offline Go suite, followed by AdditionalChecks
|
// Validate runs the selected offline Go suite, followed by AdditionalChecks
|
||||||
// when supplied, for the captured candidate.
|
// when supplied, for the captured candidate.
|
||||||
func Validate(ctx context.Context, request Request, captured Candidate) error {
|
func Validate(ctx context.Context, request Request, captured Candidate) error {
|
||||||
if err := validation.Run(ctx, validation.Request{
|
if err := validation.Run(ctx, validation.Request{
|
||||||
RepositoryDir: captured.RepositoryDir,
|
RepositoryDir: captured.RepositoryDir,
|
||||||
Commit: captured.Commit,
|
Commit: captured.Commit,
|
||||||
Output: request.Output,
|
Output: request.Output,
|
||||||
|
Settings: request.Validation,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
type State string
|
type State string
|
||||||
@@ -40,6 +41,7 @@ type Request struct {
|
|||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
Remote string
|
Remote string
|
||||||
|
Validation validation.Settings
|
||||||
AdditionalChecks func(context.Context, candidate.Candidate) error
|
AdditionalChecks func(context.Context, candidate.Candidate) error
|
||||||
AfterValidate func(context.Context, candidate.Candidate) error
|
AfterValidate func(context.Context, candidate.Candidate) error
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
@@ -55,7 +57,8 @@ type Result struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Publish validates one captured candidate and pushes only its annotated tag
|
// Publish validates one captured candidate and pushes only its annotated tag
|
||||||
// object. It runs the standard Go suite, then AdditionalChecks when supplied.
|
// object. It runs one standard Go suite, with the selected settings, then
|
||||||
|
// AdditionalChecks when supplied.
|
||||||
// A failed push receives one bounded authoritative tag query. Exact
|
// A failed push receives one bounded authoritative tag query. Exact
|
||||||
// object equality confirms publication; absence and conflict are classified,
|
// object equality confirms publication; absence and conflict are classified,
|
||||||
// while an unreadable result remains Unknown and preserves the push error.
|
// while an unreadable result remains Unknown and preserves the push error.
|
||||||
@@ -69,6 +72,7 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
|||||||
RepositoryDir: request.RepositoryDir,
|
RepositoryDir: request.RepositoryDir,
|
||||||
Product: request.Product,
|
Product: request.Product,
|
||||||
Version: request.Version,
|
Version: request.Version,
|
||||||
|
Validation: request.Validation,
|
||||||
AdditionalChecks: request.AdditionalChecks,
|
AdditionalChecks: request.AdditionalChecks,
|
||||||
Output: request.Output,
|
Output: request.Output,
|
||||||
}
|
}
|
||||||
|
|||||||
188
internal/validation/race_test.go
Normal file
188
internal/validation/race_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunUsesOneSuiteWithSelectedRaceSetting(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
race bool
|
||||||
|
}{
|
||||||
|
{name: "default"},
|
||||||
|
{name: "race", race: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
logPath := installGoShim(t, false)
|
||||||
|
err := Run(context.Background(), Request{
|
||||||
|
RepositoryDir: t.TempDir(),
|
||||||
|
Settings: Settings{Race: test.race},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commands := readGoShimLog(t, logPath)
|
||||||
|
var tests []string
|
||||||
|
for _, command := range commands {
|
||||||
|
if strings.HasPrefix(command, "test ") {
|
||||||
|
tests = append(tests, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tests) != 1 {
|
||||||
|
t.Fatalf("Go test invocations = %q, want exactly one", tests)
|
||||||
|
}
|
||||||
|
command := " " + tests[0] + " "
|
||||||
|
for _, argument := range []string{"test", "-mod=readonly", "-count=1", "./..."} {
|
||||||
|
if !strings.Contains(command, " "+argument+" ") {
|
||||||
|
t.Errorf("Go test command %q does not contain %q", tests[0], argument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := strings.Contains(command, " -race "); got != test.race {
|
||||||
|
t.Errorf("Go test command %q has race=%t, want %t", tests[0], got, test.race)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) {
|
||||||
|
logPath := installGoShim(t, true)
|
||||||
|
err := Run(context.Background(), Request{
|
||||||
|
RepositoryDir: t.TempDir(),
|
||||||
|
Settings: Settings{Race: true},
|
||||||
|
})
|
||||||
|
var failure *Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != GoTest {
|
||||||
|
t.Fatalf("Run() error = %v, want a go-test validation failure", err)
|
||||||
|
}
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if !errors.As(err, &exitError) {
|
||||||
|
t.Fatalf("Run() error = %v, want the command failure preserved", err)
|
||||||
|
}
|
||||||
|
commands := readGoShimLog(t, logPath)
|
||||||
|
var tests []string
|
||||||
|
for _, command := range commands {
|
||||||
|
if strings.HasPrefix(command, "test ") {
|
||||||
|
tests = append(tests, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tests) != 1 || !strings.Contains(" "+tests[0]+" ", " -race ") {
|
||||||
|
t.Fatalf("Go test invocations = %q, want one race-enabled invocation and no fallback", tests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunExecutesRaceEnabledSuiteOnHost(t *testing.T) {
|
||||||
|
if !raceSupported(runtime.GOOS, runtime.GOARCH) {
|
||||||
|
t.Skipf("Go race detector is not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
||||||
|
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
||||||
|
t.Skip("Go race detector requires CGO_ENABLED=1")
|
||||||
|
}
|
||||||
|
cc, err := exec.Command("go", "env", "CC").Output()
|
||||||
|
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
||||||
|
t.Skip("Go race detector requires a C compiler")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
||||||
|
t.Skip("Go race detector C compiler is unavailable")
|
||||||
|
}
|
||||||
|
module := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte("module example.com/racefixture\n\ngo 1.25.0\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(module, "race_test.go"), []byte(`package racefixture
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRaceFixture(t *testing.T) {}
|
||||||
|
`), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Run(context.Background(), Request{RepositoryDir: module, Settings: Settings{Race: true}}); err != nil {
|
||||||
|
t.Fatalf("race-enabled Go suite failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func installGoShim(t *testing.T, failRace bool) string {
|
||||||
|
t.Helper()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
||||||
|
}
|
||||||
|
directory := t.TempDir()
|
||||||
|
logPath := filepath.Join(directory, "go-commands")
|
||||||
|
settings := map[string]string{
|
||||||
|
"GOHOSTOS": runtime.GOOS,
|
||||||
|
"GOHOSTARCH": runtime.GOARCH,
|
||||||
|
"CC": "", "CXX": "", "CGO_CFLAGS": "", "CGO_CPPFLAGS": "",
|
||||||
|
"CGO_CXXFLAGS": "", "CGO_ENABLED": "1", "CGO_LDFLAGS": "",
|
||||||
|
"GOCACHE": "", "GOCACHEPROG": "", "GOMODCACHE": "", "GOPATH": "",
|
||||||
|
"GOROOT": "", "GOTMPDIR": "", "PKG_CONFIG": "",
|
||||||
|
}
|
||||||
|
encodedSettings, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
failBlock := ""
|
||||||
|
if failRace {
|
||||||
|
failBlock = `
|
||||||
|
for argument in "$@"; do
|
||||||
|
if [ "$argument" = "-race" ]; then
|
||||||
|
echo "race detector prerequisite unavailable" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
`
|
||||||
|
}
|
||||||
|
script := fmt.Sprintf(`#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||||
|
if [ "${1-}" = "env" ]; then
|
||||||
|
printf '%%s\n' '%s'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
%s
|
||||||
|
exit 0
|
||||||
|
`, encodedSettings, failBlock)
|
||||||
|
shimPath := filepath.Join(directory, "go")
|
||||||
|
if err := os.WriteFile(shimPath, []byte(script), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := directory
|
||||||
|
if current := os.Getenv("PATH"); current != "" {
|
||||||
|
path += string(os.PathListSeparator) + current
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", path)
|
||||||
|
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||||
|
return logPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGoShimLog(t *testing.T, path string) []string {
|
||||||
|
t.Helper()
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func raceSupported(goos, goarch string) bool {
|
||||||
|
switch goos {
|
||||||
|
case "darwin":
|
||||||
|
return goarch == "amd64" || goarch == "arm64"
|
||||||
|
case "freebsd", "linux":
|
||||||
|
return goarch == "amd64" || goarch == "arm64" || goarch == "ppc64le"
|
||||||
|
case "windows":
|
||||||
|
return goarch == "amd64"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Settings selects shared validation behavior. Its zero value runs the current
|
// Settings selects shared validation behavior.
|
||||||
// default Go suite.
|
type Settings struct {
|
||||||
type Settings struct{}
|
Race bool
|
||||||
|
}
|
||||||
|
|
||||||
// Request identifies the captured source and output used by shared validation.
|
// Request identifies the captured source and output used by shared validation.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
@@ -32,8 +33,12 @@ func Run(ctx context.Context, request Request) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &Failure{Check: GoTest, Cause: err}
|
return &Failure{Check: GoTest, Cause: err}
|
||||||
}
|
}
|
||||||
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output,
|
args := []string{"test", "-mod=readonly", "-count=1"}
|
||||||
"test", "-mod=readonly", "-count=1", "./...")
|
if request.Settings.Race {
|
||||||
|
args = append(args, "-race")
|
||||||
|
}
|
||||||
|
args = append(args, "./...")
|
||||||
|
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &Failure{Check: GoTest, Cause: err}
|
return &Failure{Check: GoTest, Cause: err}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ type PublicationResult struct {
|
|||||||
State PublicationState // publication outcome for this invocation
|
State PublicationState // publication outcome for this invocation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish always runs the offline Go test suite for the captured candidate,
|
// Publish always runs one offline Go test suite for the captured candidate;
|
||||||
// then calls AdditionalChecks when supplied. It optionally builds selected
|
// Validation.Go.Race enables the race detector for that suite. It then calls
|
||||||
|
// AdditionalChecks when supplied. It optionally builds selected
|
||||||
// targets in temporary storage outside the worktree and pushes the exact
|
// targets in temporary storage outside the worktree and pushes the exact
|
||||||
// annotated tag object to the selected remote. It returns a result with errors: preflight
|
// annotated tag object to the selected remote. It returns a result with errors: preflight
|
||||||
// failures are not_attempted, failed authoritative guards are conflict, and
|
// failures are not_attempted, failed authoritative guards are conflict, and
|
||||||
@@ -65,6 +66,7 @@ func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, er
|
|||||||
Product: candidateInput.Product,
|
Product: candidateInput.Product,
|
||||||
Version: candidateInput.Version,
|
Version: candidateInput.Version,
|
||||||
Remote: request.Remote,
|
Remote: request.Remote,
|
||||||
|
Validation: candidateInput.Validation,
|
||||||
AdditionalChecks: candidateInput.AdditionalChecks,
|
AdditionalChecks: candidateInput.AdditionalChecks,
|
||||||
AfterValidate: candidateInput.AfterValidate,
|
AfterValidate: candidateInput.AfterValidate,
|
||||||
Output: candidateInput.Output,
|
Output: candidateInput.Output,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -93,6 +94,35 @@ func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks(t *testing.T) {
|
||||||
|
requireRacePrerequisites(t)
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
raceMarker := filepath.Join(t.TempDir(), "race-enabled")
|
||||||
|
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
||||||
|
checks := 0
|
||||||
|
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Validation: releasekit.ValidationSpec{
|
||||||
|
Go: releasekit.GoValidationSpec{Race: true},
|
||||||
|
},
|
||||||
|
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
||||||
|
checks++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() error = %v", err)
|
||||||
|
}
|
||||||
|
if checks != 1 {
|
||||||
|
t.Fatalf("additional checks ran %d times, want once", checks)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(raceMarker); err != nil {
|
||||||
|
t.Fatalf("race-enabled fixture did not run: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testing.T) {
|
func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testing.T) {
|
||||||
repo := newPublishFixture(t)
|
repo := newPublishFixture(t)
|
||||||
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
||||||
@@ -133,6 +163,27 @@ func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireRacePrerequisites(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
supported := runtime.GOOS == "darwin" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") ||
|
||||||
|
(runtime.GOOS == "freebsd" || runtime.GOOS == "linux") && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le") ||
|
||||||
|
runtime.GOOS == "windows" && runtime.GOARCH == "amd64"
|
||||||
|
if !supported {
|
||||||
|
t.Skipf("Go race detector is not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
||||||
|
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
||||||
|
t.Skip("Go race detector requires CGO_ENABLED=1")
|
||||||
|
}
|
||||||
|
cc, err := exec.Command("go", "env", "CC").Output()
|
||||||
|
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
||||||
|
t.Skip("Go race detector requires a C compiler")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
||||||
|
t.Skip("Go race detector C compiler is unavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckReturnsPublicValidationError(t *testing.T) {
|
func TestCheckReturnsPublicValidationError(t *testing.T) {
|
||||||
repo := newPublishFixture(t)
|
repo := newPublishFixture(t)
|
||||||
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
||||||
@@ -627,6 +678,25 @@ func TestFixture(t *testing.T) {
|
|||||||
t.Fatal("fixture standard suite failure")
|
t.Fatal("fixture standard suite failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`)
|
||||||
|
writeFile(t, work, "race_fixture_test.go", `//go:build race
|
||||||
|
|
||||||
|
package fixture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRaceBuildSelection(t *testing.T) {
|
||||||
|
marker := os.Getenv("RELEASEKIT_RACE_MARKER")
|
||||||
|
if marker == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(marker, []byte("race enabled"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
`)
|
`)
|
||||||
writeFile(t, work, "docs/releases/v1.2.3.md", releaseNote)
|
writeFile(t, work, "docs/releases/v1.2.3.md", releaseNote)
|
||||||
writeFile(t, work, "source.txt", "candidate")
|
writeFile(t, work, "source.txt", "candidate")
|
||||||
|
|||||||
12
validation_test.go
Normal file
12
validation_test.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package releasekit
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCandidateRequestMapsRaceValidation(t *testing.T) {
|
||||||
|
request := candidateRequest(ReleaseRequest{
|
||||||
|
Validation: ValidationSpec{Go: GoValidationSpec{Race: true}},
|
||||||
|
})
|
||||||
|
if !request.Validation.Race {
|
||||||
|
t.Fatalf("candidate request validation = %#v, want race enabled", request.Validation)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user