Run validation in the selected module
This commit is contained in:
@@ -432,6 +432,21 @@ func TestBuildCancellationReturnsCompletedArtifacts(t *testing.T) {
|
||||
|
||||
func TestCheckBuildsAfterValidationAndKeepsOutputOutsideWorktree(t *testing.T) {
|
||||
repository := newPublishFixture(t)
|
||||
realGo, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
goShimDir := t.TempDir()
|
||||
goLog := filepath.Join(goShimDir, "go-commands")
|
||||
goShim := filepath.Join(goShimDir, "go")
|
||||
shimSource := "#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"$RELEASEKIT_REAL_GO_LOG\"\nexec \"$RELEASEKIT_REAL_GO\" \"$@\"\n"
|
||||
if err := os.WriteFile(goShim, []byte(shimSource), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("RELEASEKIT_REAL_GO", realGo)
|
||||
t.Setenv("RELEASEKIT_REAL_GO_LOG", goLog)
|
||||
t.Setenv("GOENV", "off")
|
||||
t.Setenv("PATH", goShimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
writeFile(t, repository.work, "guide.md", "# Guide\n[section](#guide)\n")
|
||||
gitRun(t, repository.work, "add", "guide.md")
|
||||
gitRun(t, repository.work, "commit", "-m", "add Markdown guide")
|
||||
@@ -465,6 +480,26 @@ func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixt
|
||||
if validations != 1 || candidate.Commit != gitText(t, repository.work, "rev-parse", "HEAD") {
|
||||
t.Fatalf("candidate = %#v, validation calls = %d", candidate, validations)
|
||||
}
|
||||
goCommands, err := os.ReadFile(goLog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var discoveries, suites, builds, versions int
|
||||
for _, command := range strings.Split(strings.TrimSpace(string(goCommands)), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(command, "env "):
|
||||
discoveries++
|
||||
case strings.HasPrefix(command, "test "):
|
||||
suites++
|
||||
case strings.HasPrefix(command, "build "):
|
||||
builds++
|
||||
case command == "version":
|
||||
versions++
|
||||
}
|
||||
}
|
||||
if discoveries != 2 || suites != 1 || builds != 1 || versions != 1 {
|
||||
t.Fatalf("Go commands = %q; discovery=%d suite=%d build=%d version=%d, want 2, 1, 1, 1", goCommands, discoveries, suites, builds, versions)
|
||||
}
|
||||
if got := gitText(t, repository.work, "status", "--porcelain"); got != "" {
|
||||
t.Fatalf("candidate build dirtied worktree: %s", got)
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ version must be stable `vMAJOR.MINOR.PATCH`. The committed
|
||||
product name and exact version as complete title elements. No local tag with
|
||||
that version may already exist.
|
||||
|
||||
ReleaseKit runs one `go test -mod=readonly -count=1 ./...` suite with the
|
||||
available compatible local Go toolchain. Set `Validation.Go.Race` to run that
|
||||
ReleaseKit runs one `go test -mod=readonly -count=1 ./...` suite from the
|
||||
selected module directory with the available compatible local Go toolchain.
|
||||
Set `Validation.Go.Race` to run that
|
||||
suite with `-race`; set `Validation.Go.FullVet` to run
|
||||
`go vet -mod=readonly ./...` after successful tests, with test-time vet disabled
|
||||
for the suite. Set `Validation.Go.CompileTargets` to compile packages for each
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Go command environments
|
||||
|
||||
`internal/gotool` resolves effective Go settings once for a validation or build
|
||||
operation and applies the shared environment to its Go commands. It uses
|
||||
`internal/gotool` resolves effective Go settings when an operation first needs
|
||||
a Go command and applies the shared environment to its commands. Candidate
|
||||
validation and its build callback share an operation-local owner while the
|
||||
inherited environment and persistent Go settings file remain unchanged.
|
||||
Standalone and delivery builds create their own owners. The package uses
|
||||
`internal/process` for cancellation, the working directory, output, and wrapped
|
||||
errors.
|
||||
|
||||
@@ -35,9 +38,10 @@ uses the same ownership transfer when formatting a failure.
|
||||
|
||||
## Configuration discovery
|
||||
|
||||
The owner asks the installed `go` command for `GOHOSTOS` and `GOHOSTARCH`, then
|
||||
queries effective operational settings with that host selected. Both queries
|
||||
use `GOTOOLCHAIN=local` and controlled `GOFLAGS`. Discovery sets
|
||||
The owner asks the installed `go` command for `GOHOSTOS`, `GOHOSTARCH`, and the
|
||||
`GOENV` settings-file path, then queries effective operational settings with
|
||||
that host selected. Both queries use `GOTOOLCHAIN=local` and controlled
|
||||
`GOFLAGS`. Discovery sets
|
||||
`GOEXPERIMENT=none` and clears architecture feature variables so an invalid or
|
||||
nonbaseline tuning value cannot prevent settings from being read. The
|
||||
experiment override is discovery-only.
|
||||
@@ -48,6 +52,11 @@ and native cgo/C-tool configuration into its settings. This captures values
|
||||
supplied by either the process environment or the persistent Go environment
|
||||
file.
|
||||
|
||||
The operation-local owner snapshots the inherited environment and selected
|
||||
settings file. It reuses the resolved tool across validation and its following
|
||||
build callback while those inputs match, and rediscovers after a consumer
|
||||
callback changes either input. It does not cache settings across operations.
|
||||
|
||||
Configuration discovery parses JSON from stdout. Stderr is captured separately
|
||||
and included in command-failure diagnostics, so a successful query can emit
|
||||
warnings while supplying valid configuration.
|
||||
@@ -69,9 +78,10 @@ baseline values. Effective `GOCACHE`, `GOCACHEPROG`, `GOMODCACHE`, `GOPATH`,
|
||||
`GOROOT`, and `GOTMPDIR` values are retained. Other process variables remain
|
||||
available to child commands.
|
||||
|
||||
Candidate validation uses native mode for the offline `go test` suite and
|
||||
selected full `go vet` pass. Package-compilation checks use static mode for each
|
||||
selected target and direct Go's output to the operating system null device, so
|
||||
they leave no binary artifact. Artifact builds also use static mode for
|
||||
selected-target compilation, then build the native version-check executable;
|
||||
that executable runs as a normal child process.
|
||||
Candidate validation runs the offline `go test` suite and selected full
|
||||
`go vet` pass from the selected module directory using native mode.
|
||||
Package-compilation checks use static mode for each selected target and direct
|
||||
Go's output to the operating system null device, so they leave no binary
|
||||
artifact. Artifact builds also compile selected targets in static mode, then
|
||||
build the native version-check executable, which runs as a normal child
|
||||
process.
|
||||
|
||||
@@ -42,7 +42,7 @@ reports these implemented packages:
|
||||
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. |
|
||||
| `internal/git` | Git command boundary, committed-tree reads, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
||||
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
||||
| `internal/gotool` | Effective Go configuration discovery, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
|
||||
| `internal/gotool` | Operation-local Go configuration discovery and reuse, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
|
||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||
| `internal/process` | Cancellable child-process execution, including owned stdin/stdout pipes for serialized exchanges, with per-command environment overlays and removals. |
|
||||
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||
@@ -61,10 +61,11 @@ also recorded in [ADR 0001](../adr/0001-public-facade.md) and the
|
||||
|
||||
Candidate checking captures and validates one clean committed source revision
|
||||
in `internal/candidate`. Its validation bridge calls `internal/validation` for
|
||||
the default offline Go suite, then runs product-specific checks once. The
|
||||
candidate package owns post-validation builds and local-state rechecks;
|
||||
publication owns endpoint guards, tag creation, push, and recovery around that
|
||||
same candidate validation path. `internal/validation.CandidateTree` reads
|
||||
the selected module's default offline Go suite, then runs product-specific
|
||||
checks once. Validation and its post-validation build callback share one local
|
||||
Go settings owner; the candidate package owns those builds and local-state
|
||||
rechecks; publication owns endpoint guards, tag creation, push, and recovery
|
||||
around that same candidate validation path. `internal/validation.CandidateTree` reads
|
||||
committed blobs lazily and resolves local destinations from committed entries;
|
||||
it does not consult neighboring files or traverse submodule checkouts.
|
||||
See the [shared validation guide](validation.md) for checker, Go command, and
|
||||
@@ -81,7 +82,7 @@ not a separate release matrix:
|
||||
| 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`, `TestRunCompilesTargetsAfterTestAndVetInSelectionOrder`, `TestRunAttributesTargetCompileFailureAndStops`, `TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts`, `TestCandidateRequestMapsGoValidation`, `TestCandidateRequestPreservesMarkdownObjectSelection`, `TestLoadSettingsReadsGoValidation`, `TestLoadSettingsPreservesMarkdownObjectSelection`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestMarkdownValidationRunsThroughCheckAndPublish`, `TestCheckStopsBeforeProductAndBuildCallbacksWhenMarkdownBoundaryFails`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce`, `TestDeliverPageOnlyCreatesAndReusesRelease` |
|
||||
| Committed-file validation: formatting, workspace files, local replacements, and file size | `internal/validation`, `internal/candidate`, `releasekit.Check`, `releasekit.Publish`, CLI profile conversion | `TestCheckGoFormatUsesCapturedRegularGoBlobs`, `TestCheckWorkspaceFilesReportsCommittedBlobNames`, `TestCheckLocalReplacementsParsesCommittedModuleDeclarations`, `TestCheckLocalReplacementsFollowsCommittedModuleSymlinks`, `TestCheckFileSizeUsesStrictLimitsAndDefault`, `TestCheckFileSizeAppliesLiteralPathExclusions`, `TestCheckFileSizeUsesCommittedBlobSizes`, `TestCheckFileSizeUsesOnlyCandidateFilesIndividually` |
|
||||
| Internal Markdown parsing, committed destinations, fragments, boundary observations, and public selection | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestParseMarkdownLinksAndSourceLocations`, `TestParseMarkdownDecodesEntitiesInDestinations`, `TestParseMarkdownHeadingAnchors`, `TestMarkdownCacheKeepsParsedDocumentsPerCandidatePath`, `TestCheckMarkdownLinksResolvesCommittedDestinationsAndFragments`, `TestCheckMarkdownLinksReportsMissingPathsAnchorsAndCycles`, `TestCheckMarkdownLinksUsesResolvedMarkdownIdentity`, `TestCheckMarkdownLinksReportsBoundaryAndSubmoduleObservations`, `TestCheckMarkdownLinksPreservesResolverCauseAndCancellation`, `TestCheckMarkdownLinksPreservesAnchorReadCause`, `TestRunSelectsMarkdownWithCombinedGoChecksAndReportsOutOfScopeLinks`, `TestMarkdownValidationRunsThroughCheckAndPublish`, `TestLoadSettingsPreservesMarkdownObjectSelection`, `TestValidationSpecPreservesMarkdownObjectSelection` |
|
||||
| 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`, `TestOwnerReusesSettingsUntilInheritedEnvironmentChanges`, `TestOwnerRefreshesWhenPersistentSettingsChange`, `TestCheckSharesGoDiscoveryWithAfterValidateCallback`, `TestCheckBuildsAfterValidationAndKeepsOutputOutsideWorktree`, `TestRunValidatesOnlyTheSelectedModule`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||
| Batched local Git blob reads | `internal/git`, `internal/process` | `TestBlobReaderReadsLooseAndPackedObjects`, `TestBlobReaderMissingObjectKeepsBatchSessionUsable`, `TestCandidateObjectReadsStayLocalInPartialClones` |
|
||||
| Piped child-process exchanges and cancellation cleanup | `internal/process` | `TestStartPipedExchangesBeforeInputEOFAndPreservesSettings`, `TestStartPipedCancellationAfterReadinessReapsChild`, `TestStartPipedPreservesNonzeroExit` |
|
||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||
|
||||
@@ -8,15 +8,16 @@ post-validation build callback and local-state recheck. `internal/publish`
|
||||
adds its endpoint guards, tag creation, push, and recovery around that same
|
||||
candidate path.
|
||||
|
||||
The executor runs one offline Go suite, optionally with the race detector when
|
||||
selected. When full vet is selected, it disables test-time vet for that suite
|
||||
and runs native `go vet -mod=readonly ./...` after the suite succeeds. It then
|
||||
runs `go build -mod=readonly -o <os.DevNull> ./...` in static mode for each
|
||||
selected package-compilation target, preserving target order and stopping on
|
||||
the first failure. This compiles source without producing an artifact or
|
||||
executing a target binary. Test, vet, and package-compilation failures retain
|
||||
their `go-test`, `go-vet`, and `package-compile` identities, selected target
|
||||
where applicable, and underlying causes. Its
|
||||
The executor runs one offline Go suite from the captured module directory,
|
||||
optionally with the race detector when selected. When full vet is selected, it
|
||||
disables test-time vet for that suite and runs native
|
||||
`go vet -mod=readonly ./...` from the same directory after the suite succeeds.
|
||||
It then runs `go build -mod=readonly -o <os.DevNull> ./...` there in static mode
|
||||
for each selected package-compilation target, preserving target order and
|
||||
stopping on the first failure. This compiles source without producing an
|
||||
artifact or executing a target binary. Test, vet, and package-compilation
|
||||
failures retain their `go-test`, `go-vet`, and `package-compile` identities,
|
||||
selected target where applicable, and underlying causes. Its
|
||||
request carries the captured repository directory and commit, output writer,
|
||||
and internal settings. When selected, the Go-format check loads the candidate
|
||||
tree once, reads regular Go blobs from the captured commit, and compares them
|
||||
|
||||
@@ -371,6 +371,18 @@ suite and reused discovery in an unchanged validation/build operation; changing
|
||||
environment inputs through an additional callback triggers fresh settings.
|
||||
Run Go-tool, validation, and candidate coordination tests.
|
||||
|
||||
**Completed:** Go validation commands now run from the captured module
|
||||
directory. Candidate checks and publication share a lazy operation-local Go
|
||||
settings owner with their later build callbacks; standalone and managed delivery
|
||||
builds acquire separate owners. The owner reuses discovery while the inherited
|
||||
environment and selected `GOENV` file stay unchanged, and refreshes after
|
||||
consumer callbacks when either changes. Nested-module fixtures now include
|
||||
packages for selected-module validation. Verified one suite and shared discovery
|
||||
through candidate build coordination, environment and persistent-file refresh,
|
||||
nested-suite failure and sibling isolation, selected vet/compile command
|
||||
directories and compile baseline, `go test ./...`, and
|
||||
`go run ./internal/apicheck`.
|
||||
|
||||
## Stage 7: Build the selected module and bind output evidence
|
||||
|
||||
**Outcome:** Standalone and composite builds use the correct declaration source,
|
||||
|
||||
@@ -34,6 +34,7 @@ type Request struct {
|
||||
Spec Spec
|
||||
OutputDir string
|
||||
Output io.Writer
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
type Artifact struct {
|
||||
@@ -118,7 +119,11 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
return result, fmt.Errorf("create build output directory %q: %w", result.OutputDir, err)
|
||||
}
|
||||
|
||||
goTool, err := gotool.New(ctx, repositoryDir)
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(repositoryDir)
|
||||
}
|
||||
goTool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("prepare Go toolchain: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,16 +7,17 @@ import (
|
||||
"os"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||
)
|
||||
|
||||
func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candidate.Candidate) error {
|
||||
func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candidate.Candidate, *gotool.Owner) error {
|
||||
if spec == nil || len(spec.Targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
selected := *spec
|
||||
selected.Targets = append([]Target(nil), spec.Targets...)
|
||||
return func(ctx context.Context, captured candidate.Candidate) error {
|
||||
return func(ctx context.Context, captured candidate.Candidate, owner *gotool.Owner) error {
|
||||
temporaryDir, err := tempdir.Outside(captured.RepositoryDir, "releasekit-candidate-build-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary candidate build output: %w", err)
|
||||
@@ -28,6 +29,7 @@ func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candi
|
||||
Spec: selected,
|
||||
OutputDir: temporaryDir,
|
||||
Output: output,
|
||||
ToolOwner: owner,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("build candidate: %w", err)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
@@ -37,8 +38,9 @@ type Request struct {
|
||||
Version string
|
||||
Validation validation.Settings
|
||||
AdditionalChecks func(context.Context, Candidate) error
|
||||
AfterValidate func(context.Context, Candidate) error
|
||||
AfterValidate func(context.Context, Candidate, *gotool.Owner) error
|
||||
Output io.Writer
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
func Check(ctx context.Context, request Request) (Candidate, error) {
|
||||
@@ -46,12 +48,18 @@ func Check(ctx context.Context, request Request) (Candidate, error) {
|
||||
if err != nil {
|
||||
return captured, err
|
||||
}
|
||||
request.ToolOwner = gotool.NewOwner(captured.ModuleDir)
|
||||
if err := Validate(ctx, request, captured); err != nil {
|
||||
return captured, err
|
||||
}
|
||||
if request.AfterValidate != nil {
|
||||
if err := request.AfterValidate(ctx, captured); err != nil {
|
||||
return captured, fmt.Errorf("after candidate validation: %w", err)
|
||||
callbackErr := request.AfterValidate(ctx, captured, request.ToolOwner)
|
||||
refreshErr := request.ToolOwner.Refresh(ctx)
|
||||
if callbackErr != nil {
|
||||
return captured, fmt.Errorf("after candidate validation: %w", callbackErr)
|
||||
}
|
||||
if refreshErr != nil {
|
||||
return captured, fmt.Errorf("refresh Go settings after candidate callback: %w", refreshErr)
|
||||
}
|
||||
}
|
||||
if err := Recheck(ctx, captured, ""); err != nil {
|
||||
@@ -120,18 +128,28 @@ func Capture(ctx context.Context, request Request) (Candidate, error) {
|
||||
// Validate runs the selected offline Go suite, followed by AdditionalChecks
|
||||
// when supplied, for the captured candidate.
|
||||
func Validate(ctx context.Context, request Request, captured Candidate) error {
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(captured.ModuleDir)
|
||||
}
|
||||
if err := validation.Run(ctx, validation.Request{
|
||||
RepositoryDir: captured.RepositoryDir,
|
||||
ModuleDir: captured.ModuleDir,
|
||||
Commit: captured.Commit,
|
||||
Output: request.Output,
|
||||
Settings: request.Validation,
|
||||
ToolOwner: owner,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
||||
}
|
||||
if request.AdditionalChecks != nil {
|
||||
if err := request.AdditionalChecks(ctx, captured); err != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, err)
|
||||
callbackErr := request.AdditionalChecks(ctx, captured)
|
||||
refreshErr := owner.Refresh(ctx)
|
||||
if callbackErr != nil {
|
||||
return fmt.Errorf("validate candidate %s: %w", captured.Commit, callbackErr)
|
||||
}
|
||||
if refreshErr != nil {
|
||||
return fmt.Errorf("refresh Go settings after additional checks: %w", refreshErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -3,13 +3,17 @@ package candidate
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
@@ -159,7 +163,7 @@ func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T)
|
||||
productCalls++
|
||||
return nil
|
||||
},
|
||||
AfterValidate: func(context.Context, Candidate) error {
|
||||
AfterValidate: func(context.Context, Candidate, *gotool.Owner) error {
|
||||
buildCalls++
|
||||
return nil
|
||||
},
|
||||
@@ -191,7 +195,7 @@ func TestCheckStopsBeforeProductAndBuildCallbacksWhenMarkdownBoundaryFails(t *te
|
||||
productCalls++
|
||||
return nil
|
||||
},
|
||||
AfterValidate: func(context.Context, Candidate) error {
|
||||
AfterValidate: func(context.Context, Candidate, *gotool.Owner) error {
|
||||
buildCalls++
|
||||
return nil
|
||||
},
|
||||
@@ -265,6 +269,125 @@ func TestCheckRunsDefaultOfflineGoValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRefreshesGoSettingsAfterAdditionalChecks(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
t.Setenv("GOENV", "off")
|
||||
t.Setenv("GOMODCACHE", filepath.Join(t.TempDir(), "initial-module-cache"))
|
||||
selectedCache := filepath.Join(t.TempDir(), "callback-module-cache")
|
||||
var observed string
|
||||
_, err := Check(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
Product: "Example Product",
|
||||
Version: testVersion,
|
||||
AdditionalChecks: func(context.Context, Candidate) error {
|
||||
t.Setenv("GOMODCACHE", selectedCache)
|
||||
return nil
|
||||
},
|
||||
AfterValidate: func(ctx context.Context, captured Candidate, owner *gotool.Owner) error {
|
||||
tool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := tool.Output(ctx, captured.ModuleDir, gotool.Native, gotool.Target{}, nil, "env", "GOMODCACHE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
observed = strings.TrimSpace(string(output))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if observed != selectedCache {
|
||||
t.Fatalf("Go settings after additional checks = %q, want %q", observed, selectedCache)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSharesGoDiscoveryWithAfterValidateCallback(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("the controlled Go command fixture uses a POSIX shell")
|
||||
}
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
logPath := installCandidateGoShim(t)
|
||||
t.Setenv("GOENV", "off")
|
||||
_, err := Check(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
Product: "Example Product",
|
||||
Version: testVersion,
|
||||
AfterValidate: func(ctx context.Context, captured Candidate, owner *gotool.Owner) error {
|
||||
tool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tool.Output(ctx, captured.ModuleDir, gotool.Native, gotool.Target{}, nil, "version")
|
||||
return err
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commands := strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||
var discoveries, suites, callbacks int
|
||||
for _, command := range commands {
|
||||
switch {
|
||||
case strings.HasPrefix(command, "env "):
|
||||
discoveries++
|
||||
case strings.HasPrefix(command, "test "):
|
||||
suites++
|
||||
case command == "version":
|
||||
callbacks++
|
||||
}
|
||||
}
|
||||
if discoveries != 2 || suites != 1 || callbacks != 1 {
|
||||
t.Fatalf("Go invocations = %q; discovery=%d suite=%d callback=%d, want 2, 1, 1", commands, discoveries, suites, callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
func installCandidateGoShim(t *testing.T) string {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
logPath := filepath.Join(directory, "go-commands")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS, "GOHOSTARCH": runtime.GOARCH, "GOENV": "off",
|
||||
"CC": "", "CXX": "", "CGO_CFLAGS": "", "CGO_CPPFLAGS": "", "CGO_CXXFLAGS": "",
|
||||
"CGO_ENABLED": "1", "CGO_LDFLAGS": "", "GOCACHE": "", "GOCACHEPROG": "",
|
||||
"GOMODCACHE": "", "GOPATH": "", "GOROOT": "", "GOTMPDIR": "", "PKG_CONFIG": "",
|
||||
}
|
||||
encoded, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
if [ "${1-}" = "env" ]; then
|
||||
printf '%%s\n' '%s'
|
||||
exit 0
|
||||
fi
|
||||
if [ "${1-}" = "version" ]; then
|
||||
echo 'go version fixture'
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
`, encoded)
|
||||
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 TestDefaultValidationDoesNotInheritGOFLAGSRunFilter(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
writeTestFile(t, repository, "failure_test.go", "package candidate\nimport \"testing\"\nfunc TestExpectedFailure(t *testing.T) { t.Fatal(\"expected fixture failure\") }\n")
|
||||
|
||||
@@ -34,8 +34,9 @@ type Target struct {
|
||||
// Tool holds the host target and effective operational settings resolved from
|
||||
// the process environment and Go's persistent environment file.
|
||||
type Tool struct {
|
||||
host Target
|
||||
settings map[string]string
|
||||
host Target
|
||||
settings map[string]string
|
||||
settingsFile string
|
||||
}
|
||||
|
||||
var operationalSettings = []string{
|
||||
@@ -53,10 +54,11 @@ var architectureFeatures = []string{
|
||||
// configuration from being read.
|
||||
func New(ctx context.Context, directory string) (*Tool, error) {
|
||||
var host struct {
|
||||
OS string `json:"GOHOSTOS"`
|
||||
Arch string `json:"GOHOSTARCH"`
|
||||
OS string `json:"GOHOSTOS"`
|
||||
Arch string `json:"GOHOSTARCH"`
|
||||
SettingsFile string `json:"GOENV"`
|
||||
}
|
||||
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH")
|
||||
output, err := discover(ctx, directory, discoveryEnvironment(), "go", "env", "-json", "GOHOSTOS", "GOHOSTARCH", "GOENV")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identify Go host: %w", err)
|
||||
}
|
||||
@@ -66,7 +68,7 @@ func New(ctx context.Context, directory string) (*Tool, error) {
|
||||
if host.OS == "" || host.Arch == "" {
|
||||
return nil, fmt.Errorf("Go returned an incomplete host target: %s", strings.TrimSpace(string(output)))
|
||||
}
|
||||
tool := &Tool{host: Target{OS: host.OS, Arch: host.Arch}}
|
||||
tool := &Tool{host: Target{OS: host.OS, Arch: host.Arch}, settingsFile: host.SettingsFile}
|
||||
|
||||
environment := discoveryEnvironment()
|
||||
environment["GOOS"] = tool.host.OS
|
||||
|
||||
@@ -301,6 +301,72 @@ func TestPersistentSettingsPreserveCachesAndNativeCTools(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerReusesSettingsUntilInheritedEnvironmentChanges(t *testing.T) {
|
||||
clearGoSettings(t)
|
||||
t.Setenv("GOENV", "off")
|
||||
owner := NewOwner(t.TempDir())
|
||||
|
||||
first, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first != second {
|
||||
t.Fatal("unchanged Go inputs caused settings discovery to be repeated")
|
||||
}
|
||||
|
||||
cache := filepath.Join(t.TempDir(), "changed-cache")
|
||||
t.Setenv("GOCACHE", cache)
|
||||
if err := owner.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
third, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if third == first || third.settings["GOCACHE"] != cache {
|
||||
t.Fatalf("changed inherited environment kept stale settings: tool=%p GOCACHE=%q", third, third.settings["GOCACHE"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerRefreshesWhenPersistentSettingsChange(t *testing.T) {
|
||||
clearGoSettings(t)
|
||||
firstCache := filepath.Join(t.TempDir(), "first-cache")
|
||||
secondCache := filepath.Join(t.TempDir(), "second-cache")
|
||||
settingsFile := filepath.Join(t.TempDir(), "go-env")
|
||||
writeSettings := func(cache string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(settingsFile, []byte("GOCACHE="+cache+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeSettings(firstCache)
|
||||
t.Setenv("GOENV", settingsFile)
|
||||
owner := NewOwner(t.TempDir())
|
||||
|
||||
first, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.settings["GOCACHE"] != firstCache {
|
||||
t.Fatalf("initial GOCACHE = %q, want %q", first.settings["GOCACHE"], firstCache)
|
||||
}
|
||||
writeSettings(secondCache)
|
||||
if err := owner.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := owner.Acquire(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second == first || second.settings["GOCACHE"] != secondCache {
|
||||
t.Fatalf("changed persistent settings were not rediscovered: tool=%p GOCACHE=%q", second, second.settings["GOCACHE"])
|
||||
}
|
||||
}
|
||||
|
||||
func readSettings(t *testing.T, tool *Tool, mode Mode, target Target, names ...string) map[string]string {
|
||||
t.Helper()
|
||||
args := append([]string{"env", "-json"}, names...)
|
||||
|
||||
136
internal/gotool/owner.go
Normal file
136
internal/gotool/owner.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package gotool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Owner lazily discovers and reuses one Go tool while its inherited
|
||||
// environment and persistent settings file remain unchanged.
|
||||
type Owner struct {
|
||||
directory string
|
||||
tool *Tool
|
||||
env map[string]string
|
||||
file string
|
||||
contents []byte
|
||||
fileFound bool
|
||||
}
|
||||
|
||||
// NewOwner creates an operation-local Go tool owner without running Go.
|
||||
func NewOwner(directory string) *Owner {
|
||||
return &Owner{directory: directory}
|
||||
}
|
||||
|
||||
// Acquire returns the operation's Go tool, rediscovering settings when their
|
||||
// process or persistent inputs have changed.
|
||||
func (owner *Owner) Acquire(ctx context.Context) (*Tool, error) {
|
||||
if owner.tool != nil {
|
||||
changed, err := owner.inputsChanged()
|
||||
if err == nil && !changed {
|
||||
return owner.tool, nil
|
||||
}
|
||||
}
|
||||
if err := owner.discover(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return owner.tool, nil
|
||||
}
|
||||
|
||||
// Refresh checks the owner's inputs after a consumer callback and rediscovers
|
||||
// settings when the callback changed the environment or persistent file.
|
||||
func (owner *Owner) Refresh(ctx context.Context) error {
|
||||
if owner.tool == nil {
|
||||
return nil
|
||||
}
|
||||
changed, err := owner.inputsChanged()
|
||||
if err == nil && !changed {
|
||||
return nil
|
||||
}
|
||||
return owner.discover(ctx)
|
||||
}
|
||||
|
||||
func (owner *Owner) discover(ctx context.Context) error {
|
||||
environment := inheritedEnvironment()
|
||||
tool, err := New(ctx, owner.directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settingsFile, err := resolveSettingsFile(owner.directory, tool.settingsFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contents, found, err := readSettingsFile(settingsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read Go settings file %q: %w", settingsFile, err)
|
||||
}
|
||||
owner.tool = tool
|
||||
owner.env = environment
|
||||
owner.file = settingsFile
|
||||
owner.contents = contents
|
||||
owner.fileFound = found
|
||||
return nil
|
||||
}
|
||||
|
||||
func (owner *Owner) inputsChanged() (bool, error) {
|
||||
if !sameEnvironment(owner.env, inheritedEnvironment()) {
|
||||
return true, nil
|
||||
}
|
||||
contents, found, err := readSettingsFile(owner.file)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return found != owner.fileFound || !bytes.Equal(contents, owner.contents), nil
|
||||
}
|
||||
|
||||
func inheritedEnvironment() map[string]string {
|
||||
values := make(map[string]string)
|
||||
for _, entry := range os.Environ() {
|
||||
key, value, ok := strings.Cut(entry, "=")
|
||||
if ok {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func sameEnvironment(left, right map[string]string) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for key, value := range left {
|
||||
other, ok := right[key]
|
||||
if !ok || other != value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func resolveSettingsFile(directory, filename string) (string, error) {
|
||||
if filename == "" || filename == "off" {
|
||||
return "", nil
|
||||
}
|
||||
if !filepath.IsAbs(filename) {
|
||||
filename = filepath.Join(directory, filename)
|
||||
}
|
||||
return filepath.Abs(filename)
|
||||
}
|
||||
|
||||
func readSettingsFile(filename string) ([]byte, bool, error) {
|
||||
if filename == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
contents, err := os.ReadFile(filename)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return contents, true, nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
@@ -49,7 +50,7 @@ type Request struct {
|
||||
ExpectedPushURL string
|
||||
Validation validation.Settings
|
||||
AdditionalChecks func(context.Context, candidate.Candidate) error
|
||||
AfterValidate func(context.Context, candidate.Candidate) error
|
||||
AfterValidate func(context.Context, candidate.Candidate, *gotool.Owner) error
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
@@ -120,12 +121,18 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
|
||||
result.Phase = Validation
|
||||
checks.ToolOwner = gotool.NewOwner(captured.ModuleDir)
|
||||
if err := candidate.Validate(ctx, checks, captured); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if request.AfterValidate != nil {
|
||||
if err := request.AfterValidate(ctx, captured); err != nil {
|
||||
return result, fmt.Errorf("after candidate validation: %w", err)
|
||||
callbackErr := request.AfterValidate(ctx, captured, checks.ToolOwner)
|
||||
refreshErr := checks.ToolOwner.Refresh(ctx)
|
||||
if callbackErr != nil {
|
||||
return result, fmt.Errorf("after candidate validation: %w", callbackErr)
|
||||
}
|
||||
if refreshErr != nil {
|
||||
return result, fmt.Errorf("refresh Go settings after candidate callback: %w", refreshErr)
|
||||
}
|
||||
}
|
||||
if err := candidate.Recheck(ctx, captured, ""); err != nil {
|
||||
|
||||
@@ -19,25 +19,38 @@ func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
||||
name string
|
||||
race bool
|
||||
fullVet bool
|
||||
compile []Target
|
||||
}{
|
||||
{name: "default"},
|
||||
{name: "race", race: true},
|
||||
{name: "full vet", fullVet: true},
|
||||
{name: "race and full vet", race: true, fullVet: true},
|
||||
{name: "target compilation", compile: []Target{{OS: "linux", Arch: "amd64"}}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
logPath := installGoShim(t, "", false)
|
||||
repositoryDir := t.TempDir()
|
||||
moduleDir := filepath.Join(repositoryDir, "nested module")
|
||||
if err := os.MkdirAll(moduleDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{Race: test.race, FullVet: test.fullVet},
|
||||
RepositoryDir: repositoryDir,
|
||||
ModuleDir: moduleDir,
|
||||
Settings: Settings{Race: test.race, FullVet: test.fullVet, CompileTargets: test.compile},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
commands := readGoShimLog(t, logPath)
|
||||
var tests, vets []string
|
||||
var testPosition, vetPosition int
|
||||
for _, directory := range readGoShimDirectories(t, os.Getenv("RELEASEKIT_GO_DIR_LOG")) {
|
||||
if directory != moduleDir {
|
||||
t.Errorf("Go command directory = %q, want selected module %q", directory, moduleDir)
|
||||
}
|
||||
}
|
||||
var tests, vets, builds []string
|
||||
var testPosition, vetPosition, buildPosition int
|
||||
for i, command := range commands {
|
||||
if strings.HasPrefix(command, "test ") {
|
||||
tests = append(tests, command)
|
||||
@@ -47,6 +60,10 @@ func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
||||
vets = append(vets, command)
|
||||
vetPosition = i
|
||||
}
|
||||
if strings.HasPrefix(command, "build ") {
|
||||
builds = append(builds, command)
|
||||
buildPosition = i
|
||||
}
|
||||
}
|
||||
if len(tests) != 1 {
|
||||
t.Fatalf("Go test invocations = %q, want exactly one", tests)
|
||||
@@ -81,6 +98,27 @@ func TestRunUsesOneSuiteWithGoValidationSettings(t *testing.T) {
|
||||
t.Errorf("Go vet command %q did not follow the test suite %q", vets[0], tests[0])
|
||||
}
|
||||
}
|
||||
wantBuilds := 0
|
||||
if len(test.compile) != 0 {
|
||||
wantBuilds = len(test.compile)
|
||||
}
|
||||
if len(builds) != wantBuilds {
|
||||
t.Fatalf("Go build invocations = %q, want %d", builds, wantBuilds)
|
||||
}
|
||||
if wantBuilds != 0 {
|
||||
buildCommand := " " + builds[0] + " "
|
||||
for _, argument := range []string{"build", "-mod=readonly", "-o", os.DevNull, "./..."} {
|
||||
if !strings.Contains(buildCommand, " "+argument+" ") {
|
||||
t.Errorf("Go compile command %q does not contain %q", builds[0], argument)
|
||||
}
|
||||
}
|
||||
if buildPosition <= testPosition {
|
||||
t.Errorf("Go compile command %q did not follow the test suite %q", builds[0], tests[0])
|
||||
}
|
||||
if environments := readGoShimLog(t, os.Getenv("RELEASEKIT_GO_ENV_LOG")); len(environments) != 1 || environments[0] != "linux/amd64|0" {
|
||||
t.Errorf("Go compile baseline = %q, want linux/amd64|0", environments)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -111,6 +149,49 @@ func TestRunReportsRacePrerequisiteFailureWithoutFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunValidatesOnlyTheSelectedModule(t *testing.T) {
|
||||
t.Setenv("GOENV", "off")
|
||||
repositoryDir := t.TempDir()
|
||||
nestedDir := filepath.Join(repositoryDir, "nested")
|
||||
siblingDir := filepath.Join(repositoryDir, "sibling")
|
||||
for _, directory := range []string{nestedDir, siblingDir} {
|
||||
if err := os.MkdirAll(directory, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeModule := func(directory, name, testSource string) {
|
||||
t.Helper()
|
||||
files := map[string]string{
|
||||
"go.mod": "module " + name + "\n\ngo 1.25.0\n",
|
||||
"module_test.go": testSource,
|
||||
}
|
||||
for filename, contents := range files {
|
||||
if err := os.WriteFile(filepath.Join(directory, filename), []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
passingTest := "package fixture\nimport \"testing\"\nfunc TestSelected(t *testing.T) {}\n"
|
||||
failingTest := "package fixture\nimport \"testing\"\nfunc TestSibling(t *testing.T) { t.Fatal(\"sibling suite ran\") }\n"
|
||||
writeModule(repositoryDir, "example.com/root", passingTest)
|
||||
writeModule(nestedDir, "example.com/nested", passingTest)
|
||||
writeModule(siblingDir, "example.com/sibling", failingTest)
|
||||
|
||||
if err := Run(context.Background(), Request{RepositoryDir: repositoryDir}); err != nil {
|
||||
t.Fatalf("root module validation: %v", err)
|
||||
}
|
||||
if err := Run(context.Background(), Request{RepositoryDir: repositoryDir, ModuleDir: nestedDir}); err != nil {
|
||||
t.Fatalf("nested module validation ran a sibling suite: %v", err)
|
||||
}
|
||||
|
||||
writeModule(nestedDir, "example.com/nested", failingTest)
|
||||
err := Run(context.Background(), Request{RepositoryDir: repositoryDir, ModuleDir: nestedDir})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != GoTest {
|
||||
t.Fatalf("failing nested module result = %v, want Go test failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStopsAfterGoTestFailure(t *testing.T) {
|
||||
logPath := installGoShim(t, "test", false)
|
||||
err := Run(context.Background(), Request{
|
||||
@@ -214,6 +295,7 @@ func installGoShim(t *testing.T, failure string, delayVet bool) string {
|
||||
directory := t.TempDir()
|
||||
logPath := filepath.Join(directory, "go-commands")
|
||||
envLogPath := filepath.Join(directory, "go-environment")
|
||||
directoryLogPath := filepath.Join(directory, "go-directories")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS,
|
||||
"GOHOSTARCH": runtime.GOARCH,
|
||||
@@ -263,6 +345,7 @@ fi
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
pwd -P >> "$RELEASEKIT_GO_DIR_LOG"
|
||||
if [ "${1-}" = "env" ]; then
|
||||
printf '%%s\n' '%s'
|
||||
exit 0
|
||||
@@ -281,6 +364,7 @@ exit 0
|
||||
t.Setenv("PATH", path)
|
||||
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||
t.Setenv("RELEASEKIT_GO_ENV_LOG", envLogPath)
|
||||
t.Setenv("RELEASEKIT_GO_DIR_LOG", directoryLogPath)
|
||||
return logPath
|
||||
}
|
||||
|
||||
@@ -293,6 +377,15 @@ func readGoShimLog(t *testing.T, path string) []string {
|
||||
return strings.Split(strings.TrimSpace(string(contents)), "\n")
|
||||
}
|
||||
|
||||
func readGoShimDirectories(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":
|
||||
|
||||
@@ -57,6 +57,7 @@ type Request struct {
|
||||
Commit string
|
||||
Output io.Writer
|
||||
Settings Settings
|
||||
ToolOwner *gotool.Owner
|
||||
}
|
||||
|
||||
// ReadCandidateTree loads the committed-file inventory when a selected check
|
||||
@@ -87,7 +88,15 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||
goDirectory := request.ModuleDir
|
||||
if goDirectory == "" {
|
||||
goDirectory = request.RepositoryDir
|
||||
}
|
||||
owner := request.ToolOwner
|
||||
if owner == nil {
|
||||
owner = gotool.NewOwner(goDirectory)
|
||||
}
|
||||
tool, err := owner.Acquire(ctx)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
@@ -99,19 +108,19 @@ func Run(ctx context.Context, request Request) (result error) {
|
||||
args = append(args, "-vet=off")
|
||||
}
|
||||
args = append(args, "./...")
|
||||
err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, args...)
|
||||
err = tool.Run(ctx, goDirectory, gotool.Native, gotool.Target{}, request.Output, args...)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
if request.Settings.FullVet {
|
||||
err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output, "vet", "-mod=readonly", "./...")
|
||||
err = tool.Run(ctx, goDirectory, gotool.Native, gotool.Target{}, request.Output, "vet", "-mod=readonly", "./...")
|
||||
if err != nil {
|
||||
return &Failure{Check: GoVet, Cause: err}
|
||||
}
|
||||
}
|
||||
for _, target := range request.Settings.CompileTargets {
|
||||
selected := gotool.Target{OS: target.OS, Arch: target.Arch}
|
||||
err = tool.Run(ctx, request.RepositoryDir, gotool.Static, selected, request.Output,
|
||||
err = tool.Run(ctx, goDirectory, gotool.Static, selected, request.Output,
|
||||
"build", "-mod=readonly", "-o", os.DevNull, "./...")
|
||||
if err != nil {
|
||||
return &Failure{
|
||||
|
||||
@@ -805,6 +805,7 @@ func installGitWrapper(t *testing.T, repo publishFixture, mode string) gitWrappe
|
||||
func addNestedReleaseModule(t *testing.T, repository, moduleDir, modulePath, version string) {
|
||||
t.Helper()
|
||||
writeFile(t, repository, moduleDir+"/go.mod", "module "+modulePath+"\n\ngo 1.25.0\n")
|
||||
writeFile(t, repository, moduleDir+"/fixture_test.go", "package fixture\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
|
||||
writeFile(t, repository, moduleDir+"/docs/releases/"+version+".md", "# Sample App "+version+"\n\nNested release.\n")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user