package releasekit import ( "context" "io" internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" "gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "gitea.maximumdirect.net/eric/ReleaseKit/internal/publish" "gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" ) // ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH. var ErrInvalidVersion = candidate.ErrInvalidVersion // ErrUnexpectedPushURL reports that the effective publication push URL does // not exactly match ReleaseRequest.ExpectedPushURL. var ErrUnexpectedPushURL = publish.ErrUnexpectedPushURL // Candidate identifies one committed local source revision selected for // validation. type Candidate struct { RepositoryDir string Commit string Version string } // ReleaseRequest contains the inputs used to validate and optionally build one // local candidate. Remote is retained for publication operations; Check makes // no remote availability claim. type ReleaseRequest struct { RepositoryDir string Product string Version string Remote string // ExpectedPushURL optionally requires Publish's resolved push URL to match // this string exactly before validation or tag creation. Check ignores this // publication-only expectation. A mismatch returns ErrUnexpectedPushURL. ExpectedPushURL string // 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 Build *BuildSpec // optional selected-target build after validation. Output io.Writer } // ValidationSpec selects optional shared validation checks. Its zero value // preserves the current validation behavior. type ValidationSpec struct { Go GoValidationSpec // GoFormat checks the formatting of committed regular Go source files. It // does not modify repository contents and is disabled when false. GoFormat bool // Markdown selects committed Markdown destination and fragment checking. // Nil disables the check; an empty object enables it with the default // behavior, reporting outside-checkout destinations as out of scope. Markdown *MarkdownValidationSpec // ModuleDistribution selects checks for files that affect module-source // distribution. Its zero value disables those checks. ModuleDistribution ModuleDistributionValidationSpec // FileSize selects a committed-file size check. Nil disables it; an empty // object selects the 1,000,000-byte default. FileSize *FileSizeValidationSpec } // MarkdownValidationSpec selects behavior for committed Markdown links. type MarkdownValidationSpec struct { // EnforceCheckoutBoundary fails when a local destination resolves outside // the candidate checkout. Submodule contents remain out of scope. EnforceCheckoutBoundary bool } // FileSizeValidationSpec selects a maximum size for each committed blob. type FileSizeValidationSpec struct { // LimitBytes is the maximum decimal byte count. Nil selects 1,000,000; // zero is a zero-byte limit, and negative values are invalid. LimitBytes *int64 // Exclusions are literal repository-relative file or directory paths. // A directory exclusion includes committed descendants. Exclusions []string } // ModuleDistributionValidationSpec selects independent module-source // distribution checks. type ModuleDistributionValidationSpec struct { // WorkspaceFiles rejects committed go.work and go.work.sum blobs, including // nested paths and symlink entries. It does not select Go's test workspace. WorkspaceFiles bool // LocalReplacements rejects filesystem replacements in committed go.mod // declarations, including nested modules. It does not select Go's workspace. LocalReplacements bool } // 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 // 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 // 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 // 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, 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. // Validation.GoFormat checks regular committed Go source after those selected // Go checks and before AdditionalChecks. Its zero value disables formatting. // Validation.ModuleDistribution.WorkspaceFiles rejects committed workspace // files after formatting. Its zero value disables that distribution check and // does not change Go's workspace environment. // Validation.ModuleDistribution.LocalReplacements parses committed go.mod // declarations, including nested modules, and rejects filesystem replacements // after workspace-file inspection. Its zero value disables that check. // Validation.FileSize checks each selected committed blob after module // distribution checks. Its nil value disables size checks; an empty object // enables the default limit. // Validation.Markdown checks committed local Markdown destinations and // supported fragments after file-size inspection and before AdditionalChecks. // Nil disables the check; an empty object enables it. Outside-checkout // destinations are reported as out of scope unless the selected specification // enforces the checkout boundary. Submodule contents remain out of scope. // The zero values of all selections 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 // candidate evidence only; it does not establish remote availability. func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) { checked, err := candidate.Check(ctx, candidateRequest(request)) if err != nil { return Candidate{}, convertValidationError(err) } return Candidate{ RepositoryDir: checked.RepositoryDir, Commit: checked.Commit, Version: checked.Version, }, nil } func candidateRequest(request ReleaseRequest) candidate.Request { fileSize := internalFileSizeSettings(request.Validation.FileSize) 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, CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)), GoFormat: request.Validation.GoFormat, Markdown: internalMarkdownSettings(request.Validation.Markdown), ModuleDistribution: validation.ModuleDistributionSettings{ WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles, LocalReplacements: request.Validation.ModuleDistribution.LocalReplacements, }, FileSize: fileSize, }, 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 internalMarkdownSettings(spec *MarkdownValidationSpec) *validation.MarkdownSettings { if spec == nil { return nil } return &validation.MarkdownSettings{EnforceCheckoutBoundary: spec.EnforceCheckoutBoundary} } func internalFileSizeSettings(spec *FileSizeValidationSpec) *validation.FileSizeSettings { if spec == nil { return nil } var limitBytes *int64 if spec.LimitBytes != nil { limit := *spec.LimitBytes limitBytes = &limit } return &validation.FileSizeSettings{ LimitBytes: limitBytes, Exclusions: append([]string(nil), spec.Exclusions...), } } func candidateChecks(checks func(context.Context, Candidate) error) func(context.Context, candidate.Candidate) error { if checks == nil { return nil } return func(ctx context.Context, captured candidate.Candidate) error { return checks(ctx, Candidate{ RepositoryDir: captured.RepositoryDir, Commit: captured.Commit, Version: captured.Version, }) } }