Compile packages for selected targets
This commit is contained in:
23
check.go
23
check.go
@@ -49,6 +49,11 @@ type GoValidationSpec struct {
|
||||
// FullVet runs go vet after the suite instead of the test-time vet pass.
|
||||
// Failures use the go-vet validation check identity.
|
||||
FullVet bool
|
||||
// CompileTargets checks that every package builds for each complete target
|
||||
// pair after testing and vet. Compilation uses CGO_ENABLED=0 and does not
|
||||
// produce artifacts or execute target binaries. Failures use the
|
||||
// package-compile validation check identity.
|
||||
CompileTargets []Target
|
||||
}
|
||||
|
||||
// Check validates one local, committed release candidate. It requires a clean
|
||||
@@ -56,8 +61,11 @@ type GoValidationSpec struct {
|
||||
// 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
|
||||
// AdditionalChecks when supplied. Validation.Go.Race runs that one suite with
|
||||
// the race detector, and Validation.Go.FullVet runs go vet after the suite
|
||||
// instead of test-time vet. Their zero values preserve the ordinary suite. If
|
||||
// the race detector, Validation.Go.FullVet runs go vet after the suite instead
|
||||
// of test-time vet, and Validation.Go.CompileTargets compiles all packages for
|
||||
// each selected GOOS/GOARCH pair after tests and vet with cgo disabled. Package
|
||||
// compilation produces no artifacts and does not execute target binaries.
|
||||
// Their zero values preserve the ordinary 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
|
||||
@@ -75,18 +83,23 @@ func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
||||
}
|
||||
|
||||
func candidateRequest(request ReleaseRequest) candidate.Request {
|
||||
return candidate.Request{
|
||||
result := candidate.Request{
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
Product: request.Product,
|
||||
Version: request.Version,
|
||||
Validation: validation.Settings{
|
||||
Race: request.Validation.Go.Race,
|
||||
FullVet: request.Validation.Go.FullVet,
|
||||
Race: request.Validation.Go.Race,
|
||||
FullVet: request.Validation.Go.FullVet,
|
||||
CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)),
|
||||
},
|
||||
AdditionalChecks: candidateChecks(request.AdditionalChecks),
|
||||
AfterValidate: internalbuild.CandidateCallback(internalBuildSpec(request.Build), request.Output),
|
||||
Output: request.Output,
|
||||
}
|
||||
for index, target := range request.Validation.Go.CompileTargets {
|
||||
result.Validation.CompileTargets[index] = validation.Target{OS: target.OS, Arch: target.Arch}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func candidateChecks(checks func(context.Context, Candidate) error) func(context.Context, candidate.Candidate) error {
|
||||
|
||||
@@ -130,8 +130,9 @@ type ValidationSettings struct {
|
||||
|
||||
// GoValidationSettings selects checks performed by the Go toolchain.
|
||||
type GoValidationSettings struct {
|
||||
Race bool `json:"race,omitempty"`
|
||||
FullVet bool `json:"full_vet,omitempty"`
|
||||
Race bool `json:"race,omitempty"`
|
||||
FullVet bool `json:"full_vet,omitempty"`
|
||||
CompileTargets []string `json:"compile_targets,omitempty"`
|
||||
}
|
||||
|
||||
// DeliverySettings selects the hosted Gitea release destination.
|
||||
@@ -219,6 +220,17 @@ func LoadSettings(repoDir, configPath string) (Settings, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings.Validation != nil {
|
||||
for i, target := range settings.Validation.Go.CompileTargets {
|
||||
if strings.Count(target, "/") != 1 {
|
||||
return Settings{}, fmt.Errorf("profile %q: validation.go.compile_targets[%d] must be GOOS/GOARCH", configPath, i)
|
||||
}
|
||||
goos, goarch, _ := strings.Cut(target, "/")
|
||||
if strings.TrimSpace(goos) == "" || strings.TrimSpace(goarch) == "" {
|
||||
return Settings{}, fmt.Errorf("profile %q: validation.go.compile_targets[%d] must be GOOS/GOARCH", configPath, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
|
||||
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
||||
repoDir := t.TempDir()
|
||||
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true,"full_vet":true}}}`), 0o600); err != nil {
|
||||
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -155,6 +155,9 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
||||
if settings.Validation == nil || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
||||
t.Fatalf("validation settings = %#v, want Go race and full vet enabled", settings.Validation)
|
||||
}
|
||||
if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want {
|
||||
t.Fatalf("compile targets = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleProfileLoads(t *testing.T) {
|
||||
@@ -207,6 +210,10 @@ func TestLoadSettingsRejectsInvalidProfiles(t *testing.T) {
|
||||
{name: "unknown top-level field", content: `{"product":"Example","future":{}}`, want: "unknown field"},
|
||||
{name: "build package required", content: `{"product":"Example","build":{"binary_name":"example","targets":["linux/amd64"]}}`, want: "build.main_package is required"},
|
||||
{name: "build target format", content: `{"product":"Example","build":{"main_package":".","binary_name":"example","targets":["linux"]}}`, want: "must be GOOS/GOARCH"},
|
||||
{name: "compile target format", content: `{"product":"Example","validation":{"go":{"compile_targets":["linux"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
||||
{name: "compile target missing architecture", content: `{"product":"Example","validation":{"go":{"compile_targets":["linux/"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
||||
{name: "compile target missing operating system", content: `{"product":"Example","validation":{"go":{"compile_targets":["/amd64"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
||||
{name: "compile target repeated separator", content: `{"product":"Example","validation":{"go":{"compile_targets":["linux/amd64/arm64"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
||||
{name: "unsafe binary name", content: `{"product":"Example","build":{"main_package":".","binary_name":"../example","targets":["linux/amd64"]}}`, want: "filename component"},
|
||||
{name: "unknown command field", content: `{"product":"Example","checks":[{"args":["true"],"shell":true}]}`, want: "unknown field"},
|
||||
{name: "command is required", content: `{"product":"Example","checks":[{"args":[]}]}`, want: "must include a command"},
|
||||
|
||||
@@ -159,12 +159,18 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec
|
||||
if settings == nil {
|
||||
return releasekit.ValidationSpec{}
|
||||
}
|
||||
return releasekit.ValidationSpec{
|
||||
result := releasekit.ValidationSpec{
|
||||
Go: releasekit.GoValidationSpec{
|
||||
Race: settings.Go.Race,
|
||||
FullVet: settings.Go.FullVet,
|
||||
Race: settings.Go.Race,
|
||||
FullVet: settings.Go.FullVet,
|
||||
CompileTargets: make([]releasekit.Target, len(settings.Go.CompileTargets)),
|
||||
},
|
||||
}
|
||||
for index, target := range settings.Go.CompileTargets {
|
||||
goos, goarch, _ := strings.Cut(target, "/")
|
||||
result.Go.CompileTargets[index] = releasekit.Target{OS: goos, Arch: goarch}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func candidateFailureText(err error) string {
|
||||
|
||||
@@ -116,12 +116,21 @@ func TestCandidateRequestMapsGoValidation(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, FullVet: true},
|
||||
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
||||
},
|
||||
}, nil)
|
||||
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
||||
t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation)
|
||||
}
|
||||
wantTargets := []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
||||
if len(request.Validation.Go.CompileTargets) != len(wantTargets) {
|
||||
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets)
|
||||
}
|
||||
for index := range wantTargets {
|
||||
if request.Validation.Go.CompileTargets[index] != wantTargets[index] {
|
||||
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
||||
@@ -434,7 +443,11 @@ func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
settings := cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
|
||||
settings := cli.Settings{
|
||||
Product: "Example Product",
|
||||
Validation: &cli.ValidationSettings{Go: cli.GoValidationSettings{CompileTargets: []string{"linux/amd64"}}},
|
||||
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
||||
}
|
||||
writeDeliveryProfile(t, repository, settings)
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs.
|
||||
|
||||
12
docs/cli.md
12
docs/cli.md
@@ -30,6 +30,13 @@ pass after successful tests when `validation.go.full_vet` is true. It then runs
|
||||
configured additional checks in profile order. It does not inspect remote
|
||||
availability or publish a tag.
|
||||
|
||||
When `validation.go.compile_targets` is selected, `check` compiles all packages
|
||||
for each configured `GOOS/GOARCH` pair after tests and full vet, using static
|
||||
mode with cgo disabled. This check produces no artifacts and does not execute
|
||||
target binaries. Compilation failures use the `package-compile` identity and
|
||||
show the selected target. Any artifact build selected by the separate `build`
|
||||
setting runs after shared validation and configured checks.
|
||||
|
||||
`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,
|
||||
profile, or invalid-version errors. Shared validation failures identify the
|
||||
@@ -59,6 +66,11 @@ bounded reconciliation determines whether the exact tag was accepted.
|
||||
Shared validation failures use the same check and finding presentation as
|
||||
`check`, alongside publication state and phase.
|
||||
|
||||
When `validation.go.compile_targets` is selected, package compilation runs
|
||||
after tests and full vet and before configured checks. Each failure is reported
|
||||
as `package-compile` with its selected target; this does not create build
|
||||
artifacts or execute target binaries.
|
||||
|
||||
The command returns status 0 for confirmed publication, 1 for a known
|
||||
publication failure or conflict, 2 for usage, profile, or invalid-version
|
||||
errors, and 3 when the push outcome remains unknown. Unknown status must be
|
||||
|
||||
@@ -30,13 +30,24 @@ The optional `validation` object selects shared checks implemented by ReleaseKit
|
||||
`validation.go.race` enables the race detector for the single standard Go test
|
||||
suite. `validation.go.full_vet` runs a separate native `go vet` pass after that
|
||||
suite succeeds; ReleaseKit disables test-time vet for that suite to avoid
|
||||
running vet twice. When `full_vet` is omitted or false, the test command keeps
|
||||
its existing vet behavior. Both settings are independent. For example:
|
||||
running vet twice. `validation.go.compile_targets` is an ordered array of
|
||||
`GOOS/GOARCH` pairs. After tests and selected full vet pass, ReleaseKit compiles
|
||||
all packages for each target with cgo disabled. Each pair must include both
|
||||
components; the selected Go toolchain reports unsupported complete targets.
|
||||
This source compatibility check does not name or produce binaries, run a target
|
||||
executable, or create an artifact manifest. Omitted or empty `compile_targets`
|
||||
selects no package-compilation pass. When `full_vet` is omitted or false, the
|
||||
test command keeps its existing vet behavior. These settings are independent.
|
||||
For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Example",
|
||||
"validation": {"go": {"race": true, "full_vet": true}}
|
||||
"validation": {"go": {
|
||||
"race": true,
|
||||
"full_vet": true,
|
||||
"compile_targets": ["linux/amd64", "linux/arm64"]
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -52,8 +63,12 @@ The `check` and `publish` commands run one
|
||||
access disabled, adding `-race` when `validation.go.race` is true. When
|
||||
`validation.go.full_vet` is true, they add `-vet=off` to that test command and
|
||||
run native `go vet -mod=readonly ./...` after successful tests. They then run
|
||||
each configured `checks` command once, in order, as an additional product
|
||||
check. Selected-target builds run after shared validation and product checks.
|
||||
`go build -mod=readonly -o <os.DevNull> ./...` in static mode for each
|
||||
`validation.go.compile_targets` entry, in profile order. Compile failures use
|
||||
the `package-compile` identity and include the selected target. Configured
|
||||
`checks` commands run afterward, once each in order, as additional product
|
||||
checks. Artifact builds selected by the separate `build` setting run after
|
||||
shared validation and product checks.
|
||||
Do not repeat the standard suite or vet in `checks`; doing so repeats work. The
|
||||
`build` command uses the selected build settings and writes binaries to `dist` unless
|
||||
`--output` selects another path.
|
||||
|
||||
@@ -31,9 +31,13 @@ ReleaseKit runs one `go test -mod=readonly -count=1 ./...` suite 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. It calls `AdditionalChecks` once after shared validation when
|
||||
supplied. Their zero values retain the ordinary native test behavior. ReleaseKit
|
||||
does not download a different toolchain automatically. If race prerequisites
|
||||
for the suite. Set `Validation.Go.CompileTargets` to compile packages for each
|
||||
complete target pair after tests and vet. Package compilation uses
|
||||
`CGO_ENABLED=0`, writes no artifact, and does not execute a target binary; its
|
||||
failures use the `package-compile` identity and include the target. The zero
|
||||
values retain the ordinary native test behavior and select no compilation.
|
||||
ReleaseKit calls `AdditionalChecks` once after shared validation when supplied.
|
||||
It does not download a different toolchain automatically. If race prerequisites
|
||||
are unavailable, the `go-test` check fails rather than being skipped; a selected
|
||||
full-vet failure is reported as `go-vet`. The suite uses the Go host target,
|
||||
disables workspace and module network access, and ignores ambient `GOFLAGS`,
|
||||
|
||||
@@ -36,6 +36,11 @@ If race prerequisites are unavailable, the selected suite fails as `go-test`
|
||||
instead of silently running without the race detector. Full-vet failures are
|
||||
reported as `go-vet` and stop publication before tag creation.
|
||||
|
||||
Set `Validation.Go.CompileTargets` to compile packages for each complete target
|
||||
pair after tests and vet. This uses `CGO_ENABLED=0`, produces no artifact, and
|
||||
does not execute target binaries. A failure is reported as `package-compile`
|
||||
with the selected target and prevents tag creation.
|
||||
|
||||
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
||||
and additional checks, before creating the tag. The operation then rechecks the
|
||||
candidate and remote guards, creates an annotated tag for the captured commit,
|
||||
|
||||
@@ -41,6 +41,9 @@ 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. Builds
|
||||
use static mode for selected-target compilation and the native version-check
|
||||
build. The native version executable itself runs as a normal child process.
|
||||
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.
|
||||
|
||||
@@ -46,7 +46,7 @@ these implemented packages:
|
||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||
| `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/validation` | Shared Go validation with optional race and full-vet selections, 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, full-vet, and static package-compilation target selections, 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/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||
@@ -78,7 +78,7 @@ not a separate release matrix:
|
||||
| Capability | Implementation owner | Existing evidence |
|
||||
| --- | --- | --- |
|
||||
| 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` | `TestRunUsesOneSuiteWithSelectedRaceSetting`, `TestRunReportsRacePrerequisiteFailureWithoutFallback`, `TestRunExecutesRaceEnabledSuiteOnHost`, `TestCandidateRequestMapsRaceValidation`, `TestLoadSettingsReadsRaceValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `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`, `TestRunCompilesTargetsAfterTestAndVetInSelectionOrder`, `TestRunAttributesTargetCompileFailureAndStops`, `TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts`, `TestCandidateRequestMapsGoValidation`, `TestLoadSettingsReadsGoValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce`, `TestDeliverPageOnlyCreatesAndReusesRelease` |
|
||||
| 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` |
|
||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||
|
||||
@@ -10,9 +10,13 @@ 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, using the
|
||||
same discovered Go tool and native environment. Test and vet failures retain
|
||||
their separate `go-test` and `go-vet` identities and underlying causes. Its
|
||||
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
|
||||
request carries the captured repository directory and commit, output writer,
|
||||
and internal settings. `Request.ReadCandidateTree` lets a
|
||||
selected file check request candidate contents; the Go-only path does not
|
||||
@@ -70,13 +74,14 @@ for product behavior.
|
||||
## Go command environment reuse
|
||||
|
||||
The current executor uses the [shared Go command environment](gotool.md) for
|
||||
its offline suite. The [roadmap coordination design](../roadmap/implementation.md#execution-and-ownership)
|
||||
owns the proposed test, vet, and package-compilation procedure.
|
||||
its offline suite, vet pass, and selected-target package compilation. The
|
||||
[implementation roadmap](../roadmap/implementation.md#execution-and-ownership)
|
||||
records the shared ordering across validation and later file checks.
|
||||
|
||||
Reuse `internal/gotool` for native and static environments and target handling.
|
||||
Keep toolchain and module-cache discovery in `gotool`; do not reproduce its
|
||||
environment logic in a checker. Build output names, version injection, native
|
||||
smoke checks, and artifact digests remain owned by `internal/build`.
|
||||
environment logic in a checker. Artifact output names, version injection,
|
||||
native smoke checks, and artifact digests remain owned by `internal/build`.
|
||||
|
||||
## File-check coordination
|
||||
|
||||
|
||||
@@ -271,6 +271,11 @@ Protect the selected command relationship without freezing incidental flag order
|
||||
|
||||
## Stage 3: Compile packages for selected targets
|
||||
|
||||
**Completion:** Complete. `Go.CompileTargets` and profile
|
||||
`validation.go.compile_targets` select static package compilation after tests
|
||||
and vet. Failures use `package-compile` and include the selected target;
|
||||
artifact building remains separate.
|
||||
|
||||
**Outcome:** Source compatibility checks work independently of artifacts.
|
||||
|
||||
1. Add `Go.CompileTargets` and string-array profile targets. Convert to internal
|
||||
|
||||
204
internal/validation/compile_test.go
Normal file
204
internal/validation/compile_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunCompilesTargetsAfterTestAndVetInSelectionOrder(t *testing.T) {
|
||||
logPath := installGoShim(t, "", false)
|
||||
targets := []Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"}}
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{FullVet: true, CompileTargets: targets},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
var commands []string
|
||||
for _, command := range readGoShimLog(t, logPath) {
|
||||
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
|
||||
commands = append(commands, command)
|
||||
}
|
||||
}
|
||||
if len(commands) != len(targets)+2 || !strings.HasPrefix(commands[0], "test ") || !strings.HasPrefix(commands[1], "vet ") {
|
||||
t.Fatalf("validation commands = %q, want test, vet, then one build per target", commands)
|
||||
}
|
||||
for index, target := range targets {
|
||||
command := commands[index+2]
|
||||
if !strings.HasPrefix(command, "build ") {
|
||||
t.Fatalf("command %d = %q, want package build", index+2, command)
|
||||
}
|
||||
for _, argument := range []string{"build", "-mod=readonly", "-o " + os.DevNull, "./..."} {
|
||||
if !strings.Contains(" "+command+" ", " "+argument+" ") {
|
||||
t.Errorf("build command %q does not contain %q", command, argument)
|
||||
}
|
||||
}
|
||||
if got, want := strings.Fields(command)[0], "build"; got != want {
|
||||
t.Errorf("command = %q, want %q", got, want)
|
||||
}
|
||||
if target.OS == "" || target.Arch == "" {
|
||||
t.Fatalf("test target is incomplete: %#v", target)
|
||||
}
|
||||
}
|
||||
|
||||
environments, err := os.ReadFile(os.Getenv("RELEASEKIT_GO_ENV_LOG"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := strings.Split(strings.TrimSpace(string(environments)), "\n")
|
||||
if len(got) != len(targets) {
|
||||
t.Fatalf("static build environments = %q, want %d entries", got, len(targets))
|
||||
}
|
||||
for index, target := range targets {
|
||||
if want := fmt.Sprintf("%s/%s|0", target.OS, target.Arch); got[index] != want {
|
||||
t.Errorf("static build environment %d = %q, want %q", index, got[index], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAttributesTargetCompileFailureAndStops(t *testing.T) {
|
||||
logPath := installGoShim(t, "darwin/arm64", false)
|
||||
targets := []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "amd64"}}
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{FullVet: true, CompileTargets: targets},
|
||||
})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != PackageCompile {
|
||||
t.Fatalf("Run() error = %v, want a package-compile failure", err)
|
||||
}
|
||||
if len(failure.Findings) != 1 || failure.Findings[0].Target != "darwin/arm64" {
|
||||
t.Fatalf("package compilation findings = %#v, want target darwin/arm64", failure.Findings)
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
t.Fatalf("Run() error = %v, want the Go command failure preserved", err)
|
||||
}
|
||||
var commands []string
|
||||
for _, command := range readGoShimLog(t, logPath) {
|
||||
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
|
||||
commands = append(commands, command)
|
||||
}
|
||||
}
|
||||
if len(commands) != 4 || !strings.HasPrefix(commands[0], "test ") || !strings.HasPrefix(commands[1], "vet ") || !strings.HasPrefix(commands[2], "build ") || !strings.HasPrefix(commands[3], "build ") {
|
||||
t.Fatalf("validation commands after target failure = %q, want test, vet, two builds", commands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsIncompleteCompileTargets(t *testing.T) {
|
||||
installGoShim(t, "", false)
|
||||
for _, target := range []Target{{OS: "linux"}, {Arch: "amd64"}, {OS: " ", Arch: "amd64"}, {OS: "linux/other", Arch: "amd64"}} {
|
||||
err := Run(context.Background(), Request{
|
||||
RepositoryDir: t.TempDir(),
|
||||
Settings: Settings{CompileTargets: []Target{target}},
|
||||
})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) || failure.Check != PackageCompile {
|
||||
t.Errorf("Run(%#v) error = %v, want a package-compile validation failure", target, err)
|
||||
continue
|
||||
}
|
||||
if len(failure.Findings) != 1 || failure.Findings[0].Target != target.OS+"/"+target.Arch {
|
||||
t.Errorf("Run(%#v) findings = %#v, want the incomplete target", target, failure.Findings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts(t *testing.T) {
|
||||
fixtures := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
}{
|
||||
{
|
||||
name: "source library",
|
||||
files: map[string]string{
|
||||
"library.go": "package fixture\n\nconst Value = 1\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single main",
|
||||
files: map[string]string{
|
||||
"library.go": "package fixture\n\nconst Value = 1\n",
|
||||
"cmd/app/main.go": "package main\n\nfunc main() {}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple mains with repeated basenames",
|
||||
files: map[string]string{
|
||||
"library.go": "package fixture\n\nconst Value = 1\n",
|
||||
"cmd/one/tool/main.go": "package main\n\nfunc main() {}\n",
|
||||
"cmd/two/tool/main.go": "package main\n\nfunc main() {}\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, fixture := range fixtures {
|
||||
t.Run(fixture.name, func(t *testing.T) {
|
||||
module := writeCompileFixture(t, fixture.files)
|
||||
before := readCompileFixture(t, module, fixture.files)
|
||||
var targets []Target
|
||||
if fixture.name == "source library" {
|
||||
targets = []Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64"}, {OS: "darwin", Arch: "arm64"}}
|
||||
} else {
|
||||
targets = []Target{{OS: "linux", Arch: "amd64"}}
|
||||
}
|
||||
if err := Run(context.Background(), Request{
|
||||
RepositoryDir: module,
|
||||
Settings: Settings{CompileTargets: targets},
|
||||
}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
after := readCompileFixture(t, module, fixture.files)
|
||||
for path, contents := range before {
|
||||
if string(after[path]) != string(contents) {
|
||||
t.Errorf("candidate file %q changed during compilation", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeCompileFixture(t *testing.T, files map[string]string) string {
|
||||
t.Helper()
|
||||
module := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte("module example.com/compilefixture\n\ngo 1.25.0\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for path, contents := range files {
|
||||
filename := filepath.Join(module, filepath.FromSlash(path))
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return module
|
||||
}
|
||||
|
||||
func readCompileFixture(t *testing.T, module string, files map[string]string) map[string][]byte {
|
||||
t.Helper()
|
||||
contents := make(map[string][]byte, len(files)+1)
|
||||
for _, path := range append([]string{"go.mod"}, fixturePaths(files)...) {
|
||||
data, err := os.ReadFile(filepath.Join(module, filepath.FromSlash(path)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents[path] = data
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
func fixturePaths(files map[string]string) []string {
|
||||
paths := make([]string, 0, len(files))
|
||||
for path := range files {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
@@ -13,6 +13,8 @@ const (
|
||||
GoTest Check = "go-test"
|
||||
// GoVet identifies the separate full vet pass.
|
||||
GoVet Check = "go-vet"
|
||||
// PackageCompile identifies selected-target package compilation.
|
||||
PackageCompile Check = "package-compile"
|
||||
)
|
||||
|
||||
// Finding describes a validation observation at an optional source location.
|
||||
|
||||
@@ -213,6 +213,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")
|
||||
settings := map[string]string{
|
||||
"GOHOSTOS": runtime.GOOS,
|
||||
"GOHOSTARCH": runtime.GOARCH,
|
||||
@@ -250,8 +251,15 @@ if [ "${1-}" = "vet" ] && [ "%s" = "vet" ]; then
|
||||
echo "Go vet failed" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "${1-}" = "build" ]; then
|
||||
printf '%%s/%%s|%%s\n' "${GOOS-}" "${GOARCH-}" "${CGO_ENABLED-}" >> "$RELEASEKIT_GO_ENV_LOG"
|
||||
if [ "%s" = "build" ] || [ "%s" = "${GOOS-}/${GOARCH-}" ]; then
|
||||
echo "Go build failed" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
%s
|
||||
`, failure, failure, failure, vetDelay)
|
||||
`, failure, failure, failure, failure, failure, vetDelay)
|
||||
script := fmt.Sprintf(`#!/bin/sh
|
||||
set -eu
|
||||
printf '%%s\n' "$*" >> "$RELEASEKIT_GO_LOG"
|
||||
@@ -272,6 +280,7 @@ exit 0
|
||||
}
|
||||
t.Setenv("PATH", path)
|
||||
t.Setenv("RELEASEKIT_GO_LOG", logPath)
|
||||
t.Setenv("RELEASEKIT_GO_ENV_LOG", envLogPath)
|
||||
return logPath
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,25 @@ package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
)
|
||||
|
||||
// Settings selects shared validation behavior.
|
||||
type Settings struct {
|
||||
Race bool
|
||||
FullVet bool
|
||||
Race bool
|
||||
FullVet bool
|
||||
CompileTargets []Target
|
||||
}
|
||||
|
||||
// Target selects one package-compilation target.
|
||||
type Target struct {
|
||||
OS string
|
||||
Arch string
|
||||
}
|
||||
|
||||
// Request identifies the captured source and output used by shared validation.
|
||||
@@ -30,6 +40,17 @@ func (request Request) ReadCandidateTree(ctx context.Context) (*CandidateTree, e
|
||||
|
||||
// Run executes shared validation for the captured candidate.
|
||||
func Run(ctx context.Context, request Request) error {
|
||||
for _, target := range request.Settings.CompileTargets {
|
||||
if strings.TrimSpace(target.OS) == "" || strings.TrimSpace(target.Arch) == "" || strings.Contains(target.OS, "/") || strings.Contains(target.Arch, "/") {
|
||||
return &Failure{
|
||||
Check: PackageCompile,
|
||||
Findings: []Finding{{
|
||||
Target: target.OS + "/" + target.Arch,
|
||||
Message: "target must be one complete GOOS/GOARCH pair",
|
||||
}},
|
||||
}
|
||||
}
|
||||
}
|
||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
@@ -52,5 +73,20 @@ func Run(ctx context.Context, request Request) error {
|
||||
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,
|
||||
"build", "-mod=readonly", "-o", os.DevNull, "./...")
|
||||
if err != nil {
|
||||
return &Failure{
|
||||
Check: PackageCompile,
|
||||
Findings: []Finding{{
|
||||
Target: fmt.Sprintf("%s/%s", target.OS, target.Arch),
|
||||
Message: "package compilation failed",
|
||||
}},
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ type PublicationResult struct {
|
||||
|
||||
// Publish always runs one offline Go test suite for the captured candidate.
|
||||
// Validation.Go.Race enables the race detector for that suite, and
|
||||
// Validation.Go.FullVet runs go vet afterward instead of test-time vet. It then
|
||||
// calls AdditionalChecks when supplied. It optionally builds selected
|
||||
// Validation.Go.FullVet runs go vet afterward instead of test-time vet, and
|
||||
// Validation.Go.CompileTargets compiles all packages for each selected target
|
||||
// with cgo disabled after tests and vet. Compilation produces no artifacts and
|
||||
// does not execute target binaries. It then calls AdditionalChecks when
|
||||
// supplied. It optionally builds selected
|
||||
// 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
|
||||
// failures are not_attempted, failed authoritative guards are conflict, and
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// ValidationCheck identifies a shared validation check that failed.
|
||||
// Current checks include "go-test" and "go-vet".
|
||||
// Current checks include "go-test", "go-vet", and "package-compile".
|
||||
type ValidationCheck string
|
||||
|
||||
// ValidationFinding describes a failed check observation at an optional
|
||||
|
||||
@@ -9,11 +9,23 @@ import (
|
||||
|
||||
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
||||
request := candidateRequest(ReleaseRequest{
|
||||
Validation: ValidationSpec{Go: GoValidationSpec{Race: true, FullVet: true}},
|
||||
Validation: ValidationSpec{Go: GoValidationSpec{
|
||||
Race: true, FullVet: true,
|
||||
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
||||
}},
|
||||
})
|
||||
if !request.Validation.Race || !request.Validation.FullVet {
|
||||
t.Fatalf("candidate request validation = %#v, want race and full vet enabled", request.Validation)
|
||||
}
|
||||
want := []validation.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
||||
if len(request.Validation.CompileTargets) != len(want) {
|
||||
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.CompileTargets, want)
|
||||
}
|
||||
for index := range want {
|
||||
if request.Validation.CompileTargets[index] != want[index] {
|
||||
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.CompileTargets, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationErrorPreservesGoVetIdentity(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user