239 lines
10 KiB
Go
239 lines
10 KiB
Go
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. ModuleDir is the absolute selected module directory, ModulePath
|
|
// is its declared Go import path, and Tag is the complete derived Git tag.
|
|
type Candidate struct {
|
|
RepositoryDir string
|
|
ModuleDir string
|
|
ModulePath string
|
|
Tag 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
|
|
// ModuleDir selects a repository-relative module directory; empty selects
|
|
// the repository root.
|
|
ModuleDir 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 {
|
|
// SharedFiles adds repository-relative committed files to optional file
|
|
// checks. Nil selects the optional defaults; an empty non-nil slice selects
|
|
// none. Explicit files are checked for presence even when file checks are off.
|
|
SharedFiles []string
|
|
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 module-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 selected module declaration and module-local
|
|
// committed note matching Product and Version, and an unused local tag. An
|
|
// empty ModuleDir selects the repository root. The module declaration
|
|
// determines the full Git tag for the semantic Version. 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 publicCandidate(checked), convertValidationError(err)
|
|
}
|
|
return publicCandidate(checked), nil
|
|
}
|
|
|
|
func publicCandidate(checked candidate.Candidate) Candidate {
|
|
return Candidate{
|
|
RepositoryDir: checked.RepositoryDir,
|
|
ModuleDir: checked.ModuleDir,
|
|
ModulePath: checked.ModulePath,
|
|
Tag: checked.Tag,
|
|
Commit: checked.Commit,
|
|
Version: checked.Version,
|
|
}
|
|
}
|
|
|
|
func candidateRequest(request ReleaseRequest) candidate.Request {
|
|
fileSize := internalFileSizeSettings(request.Validation.FileSize)
|
|
result := candidate.Request{
|
|
RepositoryDir: request.RepositoryDir,
|
|
ModuleDir: request.ModuleDir,
|
|
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)),
|
|
SharedFiles: copyStringSlice(request.Validation.SharedFiles),
|
|
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 copyStringSlice(values []string) []string {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
return append(make([]string, 0, len(values)), values...)
|
|
}
|
|
|
|
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, publicCandidate(captured))
|
|
}
|
|
}
|