281 lines
8.1 KiB
Go
281 lines
8.1 KiB
Go
// Package validation owns the shared checks run against a captured candidate.
|
|
package validation
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"go/format"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
|
)
|
|
|
|
// Settings selects shared validation behavior.
|
|
type Settings struct {
|
|
Race bool
|
|
FullVet bool
|
|
CompileTargets []Target
|
|
SharedFiles []string
|
|
GoFormat bool
|
|
Markdown *MarkdownSettings
|
|
ModuleDistribution ModuleDistributionSettings
|
|
FileSize *FileSizeSettings
|
|
}
|
|
|
|
// MarkdownSettings selects committed Markdown destination and fragment checks.
|
|
type MarkdownSettings struct {
|
|
EnforceCheckoutBoundary bool
|
|
}
|
|
|
|
// FileSizeSettings selects a per-blob limit and literal path exclusions.
|
|
type FileSizeSettings struct {
|
|
LimitBytes *int64
|
|
Exclusions []string
|
|
}
|
|
|
|
// ModuleDistributionSettings selects candidate distribution checks.
|
|
type ModuleDistributionSettings struct {
|
|
WorkspaceFiles bool
|
|
LocalReplacements bool
|
|
}
|
|
|
|
// Target selects one package-compilation target.
|
|
type Target struct {
|
|
OS string
|
|
Arch string
|
|
}
|
|
|
|
// Request identifies the captured source and output used by shared validation.
|
|
type Request struct {
|
|
RepositoryDir string
|
|
ModuleDir string
|
|
Commit string
|
|
Output io.Writer
|
|
Settings Settings
|
|
ToolOwner *gotool.Owner
|
|
}
|
|
|
|
// ReadCandidateTree loads the committed-file inventory when a selected check
|
|
// needs file-oriented input.
|
|
func (request Request) ReadCandidateTree(ctx context.Context) (*CandidateTree, error) {
|
|
return ReadCandidateTreeForScope(ctx, request.RepositoryDir, request.Commit, TreeScope{
|
|
ModuleDir: request.ModuleDir,
|
|
SharedFiles: request.Settings.SharedFiles,
|
|
})
|
|
}
|
|
|
|
// Run executes shared validation for the captured candidate.
|
|
func Run(ctx context.Context, request Request) (result error) {
|
|
if request.Settings.FileSize != nil && request.Settings.FileSize.LimitBytes != nil && *request.Settings.FileSize.LimitBytes < 0 {
|
|
return &Failure{
|
|
Check: FileSize,
|
|
Findings: []Finding{{Message: "file size limit must not be negative"}},
|
|
}
|
|
}
|
|
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",
|
|
}},
|
|
}
|
|
}
|
|
}
|
|
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}
|
|
}
|
|
args := []string{"test", "-mod=readonly", "-count=1"}
|
|
if request.Settings.Race {
|
|
args = append(args, "-race")
|
|
}
|
|
if request.Settings.FullVet {
|
|
args = append(args, "-vet=off")
|
|
}
|
|
args = append(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, 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, goDirectory, 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,
|
|
}
|
|
}
|
|
}
|
|
needsSharedSelection := len(request.Settings.SharedFiles) != 0
|
|
if request.Settings.GoFormat || request.Settings.ModuleDistribution.WorkspaceFiles || request.Settings.ModuleDistribution.LocalReplacements || request.Settings.FileSize != nil || request.Settings.Markdown != nil || needsSharedSelection {
|
|
tree, err := request.ReadCandidateTree(ctx)
|
|
if err != nil {
|
|
var failure *Failure
|
|
if errors.As(err, &failure) {
|
|
return failure
|
|
}
|
|
check := MarkdownLinks
|
|
if request.Settings.FileSize != nil {
|
|
check = FileSize
|
|
}
|
|
if request.Settings.ModuleDistribution.LocalReplacements {
|
|
check = LocalReplacements
|
|
}
|
|
if request.Settings.ModuleDistribution.WorkspaceFiles {
|
|
check = WorkspaceFiles
|
|
}
|
|
if request.Settings.GoFormat {
|
|
check = GoFormat
|
|
}
|
|
return &Failure{Check: check, Cause: err}
|
|
}
|
|
lastContentCheck := Check("")
|
|
fileSizeSettings := fileSizeSettingsForModule(request.Settings.FileSize, tree.modulePath)
|
|
defer func() {
|
|
if closeErr := tree.Close(); closeErr != nil && result == nil && lastContentCheck != "" {
|
|
result = &Failure{
|
|
Check: lastContentCheck,
|
|
Cause: fmt.Errorf("close candidate Git blob reader: %w", closeErr),
|
|
}
|
|
}
|
|
}()
|
|
if request.Settings.GoFormat {
|
|
readsBefore := tree.blobReads
|
|
if err := checkGoFormat(ctx, tree); err != nil {
|
|
return err
|
|
}
|
|
if tree.blobReads != readsBefore {
|
|
lastContentCheck = GoFormat
|
|
}
|
|
}
|
|
if request.Settings.ModuleDistribution.WorkspaceFiles {
|
|
if err := checkWorkspaceFiles(tree); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if request.Settings.ModuleDistribution.LocalReplacements {
|
|
readsBefore := tree.blobReads
|
|
if err := checkLocalReplacements(ctx, tree); err != nil {
|
|
return err
|
|
}
|
|
if tree.blobReads != readsBefore {
|
|
lastContentCheck = LocalReplacements
|
|
}
|
|
}
|
|
if request.Settings.FileSize != nil {
|
|
if err := checkFileSize(ctx, tree, fileSizeSettings); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if request.Settings.Markdown != nil {
|
|
readsBefore := tree.blobReads
|
|
if err := checkMarkdownLinks(ctx, tree, request.Settings.Markdown.EnforceCheckoutBoundary, request.Output); err != nil {
|
|
return err
|
|
}
|
|
if tree.blobReads != readsBefore {
|
|
lastContentCheck = MarkdownLinks
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkWorkspaceFiles(tree *CandidateTree) error {
|
|
var findings []Finding
|
|
for _, entry := range tree.inputEntries {
|
|
if entry.Type != "blob" {
|
|
continue
|
|
}
|
|
switch path.Base(entry.Path) {
|
|
case "go.work", "go.work.sum":
|
|
findings = append(findings, Finding{
|
|
Path: entry.Path,
|
|
Message: "workspace file is committed in the candidate",
|
|
})
|
|
}
|
|
}
|
|
if len(findings) != 0 {
|
|
return &Failure{Check: WorkspaceFiles, Findings: findings}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkGoFormat(ctx context.Context, tree *CandidateTree) error {
|
|
var findings []Finding
|
|
formattedByObjectID := make(map[string]bool)
|
|
for _, entry := range tree.inputEntries {
|
|
if entry.Type != "blob" || (entry.Mode != "100644" && entry.Mode != "100755") || !strings.HasSuffix(entry.Path, ".go") {
|
|
continue
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return &Failure{Check: GoFormat, Cause: err}
|
|
}
|
|
if formatted, ok := formattedByObjectID[entry.ObjectID]; ok {
|
|
if !formatted {
|
|
findings = append(findings, Finding{Path: entry.Path, Message: "file is not gofmt-formatted"})
|
|
}
|
|
continue
|
|
}
|
|
contents, err := tree.ReadFile(ctx, entry.Path)
|
|
if err != nil {
|
|
return &Failure{
|
|
Check: GoFormat,
|
|
Findings: []Finding{{
|
|
Path: entry.Path,
|
|
Message: "unable to inspect committed Go source",
|
|
}},
|
|
Cause: fmt.Errorf("inspect committed Go file %q: %w", entry.Path, err),
|
|
}
|
|
}
|
|
formatted, err := format.Source(contents)
|
|
if err != nil {
|
|
return &Failure{
|
|
Check: GoFormat,
|
|
Findings: []Finding{{
|
|
Path: entry.Path,
|
|
Message: "committed Go source cannot be formatted",
|
|
}},
|
|
Cause: fmt.Errorf("format committed Go file %q: %w", entry.Path, err),
|
|
}
|
|
}
|
|
isFormatted := bytes.Equal(contents, formatted)
|
|
formattedByObjectID[entry.ObjectID] = isFormatted
|
|
contents = nil
|
|
formatted = nil
|
|
if !isFormatted {
|
|
findings = append(findings, Finding{Path: entry.Path, Message: "file is not gofmt-formatted"})
|
|
}
|
|
}
|
|
if len(findings) != 0 {
|
|
return &Failure{Check: GoFormat, Findings: findings}
|
|
}
|
|
return nil
|
|
}
|