Expose Markdown validation selection
This commit is contained in:
@@ -432,6 +432,9 @@ func TestBuildCancellationReturnsCompletedArtifacts(t *testing.T) {
|
|||||||
|
|
||||||
func TestCheckBuildsAfterValidationAndKeepsOutputOutsideWorktree(t *testing.T) {
|
func TestCheckBuildsAfterValidationAndKeepsOutputOutsideWorktree(t *testing.T) {
|
||||||
repository := newPublishFixture(t)
|
repository := newPublishFixture(t)
|
||||||
|
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")
|
||||||
prepareCandidateBuild(t, repository.work, `package main
|
prepareCandidateBuild(t, repository.work, `package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -445,6 +448,7 @@ func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixt
|
|||||||
RepositoryDir: repository.work,
|
RepositoryDir: repository.work,
|
||||||
Product: "Sample App",
|
Product: "Sample App",
|
||||||
Version: "v1.2.3",
|
Version: "v1.2.3",
|
||||||
|
Validation: releasekit.ValidationSpec{Markdown: &releasekit.MarkdownValidationSpec{}},
|
||||||
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
||||||
validations++
|
validations++
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
28
check.go
28
check.go
@@ -52,6 +52,10 @@ type ValidationSpec struct {
|
|||||||
// GoFormat checks the formatting of committed regular Go source files. It
|
// GoFormat checks the formatting of committed regular Go source files. It
|
||||||
// does not modify repository contents and is disabled when false.
|
// does not modify repository contents and is disabled when false.
|
||||||
GoFormat bool
|
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
|
// ModuleDistribution selects checks for files that affect module-source
|
||||||
// distribution. Its zero value disables those checks.
|
// distribution. Its zero value disables those checks.
|
||||||
ModuleDistribution ModuleDistributionValidationSpec
|
ModuleDistribution ModuleDistributionValidationSpec
|
||||||
@@ -60,6 +64,13 @@ type ValidationSpec struct {
|
|||||||
FileSize *FileSizeValidationSpec
|
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.
|
// FileSizeValidationSpec selects a maximum size for each committed blob.
|
||||||
type FileSizeValidationSpec struct {
|
type FileSizeValidationSpec struct {
|
||||||
// LimitBytes is the maximum decimal byte count. Nil selects 1,000,000;
|
// LimitBytes is the maximum decimal byte count. Nil selects 1,000,000;
|
||||||
@@ -107,14 +118,19 @@ type GoValidationSpec struct {
|
|||||||
// Validation.GoFormat checks regular committed Go source after those selected
|
// Validation.GoFormat checks regular committed Go source after those selected
|
||||||
// Go checks and before AdditionalChecks. Its zero value disables formatting.
|
// Go checks and before AdditionalChecks. Its zero value disables formatting.
|
||||||
// Validation.ModuleDistribution.WorkspaceFiles rejects committed workspace
|
// Validation.ModuleDistribution.WorkspaceFiles rejects committed workspace
|
||||||
// files after the other selected shared checks. Its zero value disables the
|
// files after formatting. Its zero value disables that distribution check and
|
||||||
// distribution check and does not change Go's workspace environment.
|
// does not change Go's workspace environment.
|
||||||
// Validation.ModuleDistribution.LocalReplacements parses committed go.mod
|
// Validation.ModuleDistribution.LocalReplacements parses committed go.mod
|
||||||
// declarations, including nested modules, and rejects filesystem replacements
|
// declarations, including nested modules, and rejects filesystem replacements
|
||||||
// after workspace-file inspection. Its zero value disables that check.
|
// after workspace-file inspection. Its zero value disables that check.
|
||||||
// Validation.FileSize checks each selected committed blob after module
|
// Validation.FileSize checks each selected committed blob after module
|
||||||
// distribution checks. Its nil value disables size checks; an empty object
|
// distribution checks. Its nil value disables size checks; an empty object
|
||||||
// enables the default limit.
|
// 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
|
// The zero values of all selections preserve the ordinary suite. If
|
||||||
// Build selects targets, Check builds them after validation in temporary
|
// Build selects targets, Check builds them after validation in temporary
|
||||||
// storage outside the worktree, then rechecks
|
// storage outside the worktree, then rechecks
|
||||||
@@ -143,6 +159,7 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
|||||||
FullVet: request.Validation.Go.FullVet,
|
FullVet: request.Validation.Go.FullVet,
|
||||||
CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)),
|
CompileTargets: make([]validation.Target, len(request.Validation.Go.CompileTargets)),
|
||||||
GoFormat: request.Validation.GoFormat,
|
GoFormat: request.Validation.GoFormat,
|
||||||
|
Markdown: internalMarkdownSettings(request.Validation.Markdown),
|
||||||
ModuleDistribution: validation.ModuleDistributionSettings{
|
ModuleDistribution: validation.ModuleDistributionSettings{
|
||||||
WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles,
|
WorkspaceFiles: request.Validation.ModuleDistribution.WorkspaceFiles,
|
||||||
LocalReplacements: request.Validation.ModuleDistribution.LocalReplacements,
|
LocalReplacements: request.Validation.ModuleDistribution.LocalReplacements,
|
||||||
@@ -159,6 +176,13 @@ func candidateRequest(request ReleaseRequest) candidate.Request {
|
|||||||
return result
|
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 {
|
func internalFileSizeSettings(spec *FileSizeValidationSpec) *validation.FileSizeSettings {
|
||||||
if spec == nil {
|
if spec == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -130,12 +130,20 @@ type ValidationSettings struct {
|
|||||||
// GoFormat checks the formatting of committed regular Go source files.
|
// GoFormat checks the formatting of committed regular Go source files.
|
||||||
// Omitted or false disables the check.
|
// Omitted or false disables the check.
|
||||||
GoFormat bool `json:"go_format,omitempty"`
|
GoFormat bool `json:"go_format,omitempty"`
|
||||||
|
Markdown *MarkdownSettings `json:"markdown,omitempty"`
|
||||||
ModuleDistribution ModuleDistributionSettings `json:"module_distribution,omitempty"`
|
ModuleDistribution ModuleDistributionSettings `json:"module_distribution,omitempty"`
|
||||||
// FileSize checks the size of each committed blob. Null or omission disables
|
// FileSize checks the size of each committed blob. Null or omission disables
|
||||||
// the check; an empty object enables the default limit.
|
// the check; an empty object enables the default limit.
|
||||||
FileSize *FileSizeSettings `json:"file_size,omitempty"`
|
FileSize *FileSizeSettings `json:"file_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkdownSettings selects committed Markdown destination and fragment checks.
|
||||||
|
type MarkdownSettings struct {
|
||||||
|
// EnforceCheckoutBoundary fails local destinations that resolve outside
|
||||||
|
// the candidate checkout. Submodule contents remain out of scope.
|
||||||
|
EnforceCheckoutBoundary bool `json:"enforce_checkout_boundary,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// FileSizeSettings selects the maximum committed-file size and literal exclusions.
|
// FileSizeSettings selects the maximum committed-file size and literal exclusions.
|
||||||
type FileSizeSettings struct {
|
type FileSizeSettings struct {
|
||||||
LimitBytes *int64 `json:"limit_bytes,omitempty"`
|
LimitBytes *int64 `json:"limit_bytes,omitempty"`
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) {
|
|||||||
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
||||||
repoDir := t.TempDir()
|
repoDir := t.TempDir()
|
||||||
profilePath := filepath.Join(repoDir, "releasekit.json")
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go_format":true,"module_distribution":{"workspace_files":true,"local_replacements":true},"file_size":{"limit_bytes":0,"exclusions":["assets"]},"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go_format":true,"markdown":{"enforce_checkout_boundary":true},"module_distribution":{"workspace_files":true,"local_replacements":true},"file_size":{"limit_bytes":0,"exclusions":["assets"]},"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if settings.Validation == nil || !settings.Validation.GoFormat || !settings.Validation.ModuleDistribution.WorkspaceFiles || !settings.Validation.ModuleDistribution.LocalReplacements || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
if settings.Validation == nil || !settings.Validation.GoFormat || settings.Validation.Markdown == nil || !settings.Validation.Markdown.EnforceCheckoutBoundary || !settings.Validation.ModuleDistribution.WorkspaceFiles || !settings.Validation.ModuleDistribution.LocalReplacements || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
||||||
t.Fatalf("validation settings = %#v, want Go formatting, workspace inspection, race, and full vet enabled", settings.Validation)
|
t.Fatalf("validation settings = %#v, want Go formatting, workspace inspection, race, and full vet enabled", settings.Validation)
|
||||||
}
|
}
|
||||||
if settings.Validation.FileSize == nil || settings.Validation.FileSize.LimitBytes == nil || *settings.Validation.FileSize.LimitBytes != 0 || len(settings.Validation.FileSize.Exclusions) != 1 || settings.Validation.FileSize.Exclusions[0] != "assets" {
|
if settings.Validation.FileSize == nil || settings.Validation.FileSize.LimitBytes == nil || *settings.Validation.FileSize.LimitBytes != 0 || len(settings.Validation.FileSize.Exclusions) != 1 || settings.Validation.FileSize.Exclusions[0] != "assets" {
|
||||||
@@ -166,6 +166,35 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadSettingsPreservesMarkdownObjectSelection(t *testing.T) {
|
||||||
|
repoDir := t.TempDir()
|
||||||
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
profile string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "omitted", profile: `{"product":"Example","validation":{}}`},
|
||||||
|
{name: "null", profile: `{"product":"Example","validation":{"markdown":null}}`},
|
||||||
|
{name: "empty object", profile: `{"product":"Example","validation":{"markdown":{}}}`, want: true},
|
||||||
|
{name: "boundary enabled", profile: `{"product":"Example","validation":{"markdown":{"enforce_checkout_boundary":true}}}`, want: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if err := os.WriteFile(profilePath, []byte(test.profile), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
settings, err := LoadSettings(repoDir, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := settings.Validation != nil && settings.Validation.Markdown != nil
|
||||||
|
if got != test.want {
|
||||||
|
t.Fatalf("Markdown selected = %t, want %t", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadSettingsPreservesEmptyAndNullFileSize(t *testing.T) {
|
func TestLoadSettingsPreservesEmptyAndNullFileSize(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec
|
|||||||
CompileTargets: make([]releasekit.Target, len(settings.Go.CompileTargets)),
|
CompileTargets: make([]releasekit.Target, len(settings.Go.CompileTargets)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if settings.Markdown != nil {
|
||||||
|
result.Markdown = &releasekit.MarkdownValidationSpec{
|
||||||
|
EnforceCheckoutBoundary: settings.Markdown.EnforceCheckoutBoundary,
|
||||||
|
}
|
||||||
|
}
|
||||||
if settings.FileSize != nil {
|
if settings.FileSize != nil {
|
||||||
var limitBytes *int64
|
var limitBytes *int64
|
||||||
if settings.FileSize.LimitBytes != nil {
|
if settings.FileSize.LimitBytes != nil {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
ExpectedPushURL: "publication-only value",
|
ExpectedPushURL: "publication-only value",
|
||||||
Validation: &cli.ValidationSettings{
|
Validation: &cli.ValidationSettings{
|
||||||
GoFormat: true,
|
GoFormat: true,
|
||||||
|
Markdown: &cli.MarkdownSettings{EnforceCheckoutBoundary: true},
|
||||||
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
||||||
ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true, LocalReplacements: true},
|
ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true, LocalReplacements: true},
|
||||||
FileSize: &cli.FileSizeSettings{LimitBytes: &limit, Exclusions: []string{"cache"}},
|
FileSize: &cli.FileSizeSettings{LimitBytes: &limit, Exclusions: []string{"cache"}},
|
||||||
@@ -130,6 +131,9 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
if !request.Validation.GoFormat {
|
if !request.Validation.GoFormat {
|
||||||
t.Fatalf("candidate request GoFormat = false, want true")
|
t.Fatalf("candidate request GoFormat = false, want true")
|
||||||
}
|
}
|
||||||
|
if request.Validation.Markdown == nil || !request.Validation.Markdown.EnforceCheckoutBoundary {
|
||||||
|
t.Fatalf("candidate request Markdown = %#v, want boundary enforcement enabled", request.Validation.Markdown)
|
||||||
|
}
|
||||||
if !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
if !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
||||||
t.Fatalf("candidate request module distribution = %#v, want both checks enabled", request.Validation.ModuleDistribution)
|
t.Fatalf("candidate request module distribution = %#v, want both checks enabled", request.Validation.ModuleDistribution)
|
||||||
}
|
}
|
||||||
@@ -150,6 +154,58 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidationSpecPreservesMarkdownObjectSelection(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
settings *cli.MarkdownSettings
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "omitted"},
|
||||||
|
{name: "empty object", settings: &cli.MarkdownSettings{}, want: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got := validationSpec(&cli.ValidationSettings{Markdown: test.settings}).Markdown != nil
|
||||||
|
if got != test.want {
|
||||||
|
t.Fatalf("Markdown selected = %t, want %t", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCandidateCommandsRunMarkdownProfile(t *testing.T) {
|
||||||
|
repository := newCLIRepository(t)
|
||||||
|
profile, err := json.Marshal(cli.Settings{
|
||||||
|
Product: "Example Product",
|
||||||
|
Validation: &cli.ValidationSettings{
|
||||||
|
Markdown: &cli.MarkdownSettings{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeRepositoryFile(t, repository, "releasekit.json", string(profile))
|
||||||
|
writeRepositoryFile(t, repository, "guide.md", "[missing](missing.md#top)\n")
|
||||||
|
cliGitRun(t, repository, "add", "releasekit.json", "guide.md")
|
||||||
|
cliGitRun(t, repository, "commit", "-m", "configure Markdown candidate validation")
|
||||||
|
remote := filepath.Join(t.TempDir(), "origin.git")
|
||||||
|
cliGitRun(t, repository, "init", "--bare", remote)
|
||||||
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
||||||
|
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
|
||||||
|
|
||||||
|
for _, command := range []string{"check", "publish"} {
|
||||||
|
t.Run(command, func(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
args := []string{command, "--repo", repository, "v1.2.3"}
|
||||||
|
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
|
||||||
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q, want validation failure", status, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "markdown-links") || !strings.Contains(stderr.String(), "guide.md:1:") || !strings.Contains(stderr.String(), "missing.md#top") {
|
||||||
|
t.Fatalf("stderr = %q, want Markdown identity, source location, and target", stderr.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
||||||
repository := newCLIRepository(t)
|
repository := newCLIRepository(t)
|
||||||
profile, err := json.Marshal(cli.Settings{Product: "Example Product"})
|
profile, err := json.Marshal(cli.Settings{Product: "Example Product"})
|
||||||
|
|||||||
18
docs/cli.md
18
docs/cli.md
@@ -44,6 +44,14 @@ modifying files. When `validation.module_distribution.workspace_files` is true,
|
|||||||
and symlink entries, as `workspace-files`. This inspection is independent of
|
and symlink entries, as `workspace-files`. This inspection is independent of
|
||||||
`GOWORK` and ignores uncommitted files. Any artifact build selected by the
|
`GOWORK` and ignores uncommitted files. Any artifact build selected by the
|
||||||
separate `build` setting runs after shared validation and configured checks.
|
separate `build` setting runs after shared validation and configured checks.
|
||||||
|
When `validation.markdown` is a non-null object, `check` checks committed local
|
||||||
|
Markdown destinations and supported heading fragments after file-size
|
||||||
|
inspection. An empty object enables the check; omission or `null` disables it.
|
||||||
|
Outside-checkout links are written as out-of-scope observations to standard
|
||||||
|
error by default. Setting `validation.markdown.enforce_checkout_boundary` to
|
||||||
|
true makes those links fail; submodule-content observations remain
|
||||||
|
nonfailures. Failures use the `markdown-links` identity with the source location
|
||||||
|
and target.
|
||||||
|
|
||||||
`check` writes errors and diagnostics to standard error. It returns status 0
|
`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,
|
after local validation, 1 after a candidate validation failure, and 2 for usage,
|
||||||
@@ -64,8 +72,9 @@ candidate: the single standard offline Go test suite runs first, with `-race`
|
|||||||
when `validation.go.race` is true. When `validation.go.full_vet` is true,
|
when `validation.go.race` is true. When `validation.go.full_vet` is true,
|
||||||
test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful
|
test-time vet is disabled and `go vet -mod=readonly ./...` runs after successful
|
||||||
tests. Selected package compilation and committed Go formatting follow. The
|
tests. Selected package compilation and committed Go formatting follow. The
|
||||||
selected workspace-file check then runs, followed by configured checks in
|
selected file checks then run in order: workspace files, local replacements,
|
||||||
profile order. It then creates an annotated local tag
|
file size, and Markdown. Configured checks run afterward in profile order. It
|
||||||
|
then creates an annotated local tag
|
||||||
and pushes only its exact tag object. It prints the confirmed version,
|
and pushes only its exact tag object. It prints the confirmed version,
|
||||||
candidate commit, and tag object
|
candidate commit, and tag object
|
||||||
on success. Validation
|
on success. Validation
|
||||||
@@ -97,6 +106,11 @@ disables the check; an empty object enables the 1,000,000 decimal-byte default.
|
|||||||
Oversized files use the `file-size` identity and report their path and measured
|
Oversized files use the `file-size` identity and report their path and measured
|
||||||
size against the limit. See the [configuration reference](config.md) for
|
size against the limit. See the [configuration reference](config.md) for
|
||||||
literal exclusions and the current-candidate scope.
|
literal exclusions and the current-candidate scope.
|
||||||
|
When `validation.markdown` is a non-null object, `publish` checks committed
|
||||||
|
Markdown destinations after file-size inspection and before configured checks.
|
||||||
|
It reports outside-checkout links to standard error unless boundary enforcement
|
||||||
|
is selected; submodule contents remain out of scope. Markdown failures stop
|
||||||
|
publication before product checks or tag creation.
|
||||||
|
|
||||||
The optional profile field `expected_push_url` applies only to `publish`. A
|
The optional profile field `expected_push_url` applies only to `publish`. A
|
||||||
nonempty value must exactly match the effective push URL resolved by Git;
|
nonempty value must exactly match the effective push URL resolved by Git;
|
||||||
|
|||||||
@@ -46,6 +46,26 @@ change under formatting. It never writes formatted bytes back. Omitted or false
|
|||||||
disables the check. It runs after tests, selected full vet, and selected package
|
disables the check. It runs after tests, selected full vet, and selected package
|
||||||
compilation, before configured product checks.
|
compilation, before configured product checks.
|
||||||
|
|
||||||
|
When `validation.markdown` is an object, ReleaseKit checks local destinations
|
||||||
|
and supported heading fragments in committed regular Markdown files. Omission
|
||||||
|
or `null` disables the check; `{}` enables it. Schemed and protocol-relative
|
||||||
|
links are skipped, URI query components do not affect path checks, and other
|
||||||
|
existing files or directories receive existence checks only. Missing local
|
||||||
|
destinations, symlink cycles, and missing Markdown headings fail with the
|
||||||
|
`markdown-links` identity. Outside-checkout destinations are reported as out
|
||||||
|
of scope by default. Set
|
||||||
|
`validation.markdown.enforce_checkout_boundary` to `true` to fail those links;
|
||||||
|
submodule contents remain out of scope under either setting.
|
||||||
|
|
||||||
|
The parser recognizes inline links, images, reference links, and CommonMark
|
||||||
|
autolinks; link-like text in code is ignored, and raw HTML destinations are
|
||||||
|
outside this check. Relative paths resolve from the source document in the
|
||||||
|
captured commit. Query components are ignored for file existence, and percent
|
||||||
|
escapes in local paths and fragments are decoded once. Fragments are compared
|
||||||
|
in lowercase with anchors derived from visible ATX or Setext heading text.
|
||||||
|
Anchors keep Unicode letters and numbers, underscores, and hyphens; spaces
|
||||||
|
become hyphens, and repeated headings receive numeric suffixes.
|
||||||
|
|
||||||
When `validation.module_distribution.workspace_files` is true, it reports
|
When `validation.module_distribution.workspace_files` is true, it reports
|
||||||
committed blob entries whose basename is `go.work` or `go.work.sum`, including
|
committed blob entries whose basename is `go.work` or `go.work.sum`, including
|
||||||
nested paths and symlinks. It checks candidate distribution contents regardless
|
nested paths and symlinks. It checks candidate distribution contents regardless
|
||||||
@@ -71,6 +91,7 @@ For example:
|
|||||||
"compile_targets": ["linux/amd64", "linux/arm64"]
|
"compile_targets": ["linux/amd64", "linux/arm64"]
|
||||||
},
|
},
|
||||||
"go_format": true,
|
"go_format": true,
|
||||||
|
"markdown": {},
|
||||||
"module_distribution": {
|
"module_distribution": {
|
||||||
"workspace_files": true,
|
"workspace_files": true,
|
||||||
"local_replacements": true
|
"local_replacements": true
|
||||||
@@ -130,6 +151,10 @@ preserve their causes.
|
|||||||
When `validation.file_size` is selected, shared validation checks committed
|
When `validation.file_size` is selected, shared validation checks committed
|
||||||
blob sizes after module-distribution checks and reports the first oversized
|
blob sizes after module-distribution checks and reports the first oversized
|
||||||
path with the `file-size` identity and measured and limit byte counts.
|
path with the `file-size` identity and measured and limit byte counts.
|
||||||
|
When `validation.markdown` is selected, Markdown checking follows file-size
|
||||||
|
inspection and runs before configured product checks. Out-of-scope observations
|
||||||
|
are written to the command's standard error and do not fail validation unless
|
||||||
|
the checkout boundary is enforced. Submodule observations remain nonfailures.
|
||||||
Configured `checks` commands run afterward, once each in order, as additional
|
Configured `checks` commands run afterward, once each in order, as additional
|
||||||
product checks. Artifact builds selected by the separate `build` setting run
|
product checks. Artifact builds selected by the separate `build` setting run
|
||||||
after shared validation and product checks.
|
after shared validation and product checks.
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ directories; excluded directories include descendants. Symlink blobs are
|
|||||||
measured by link-text bytes, and gitlinks are omitted. The check applies to the
|
measured by link-text bytes, and gitlinks are omitted. The check applies to the
|
||||||
captured candidate and reports one oversized file with its measurement as
|
captured candidate and reports one oversized file with its measurement as
|
||||||
`file-size`; it does not measure historical or aggregate size.
|
`file-size`; it does not measure historical or aggregate size.
|
||||||
|
Set `Validation.Markdown` to check committed local Markdown destinations and
|
||||||
|
supported heading fragments. Nil disables the check; an empty object enables
|
||||||
|
it with outside-checkout destinations reported as out of scope. Set
|
||||||
|
`EnforceCheckoutBoundary` to fail outside-checkout links. Submodule contents
|
||||||
|
remain out of scope under either setting. The checker runs after file-size
|
||||||
|
inspection and before `AdditionalChecks`; its nonfailure observations use the
|
||||||
|
request's output writer. Markdown failures use the `markdown-links` identity
|
||||||
|
and include the source location and destination.
|
||||||
|
|
||||||
|
Use these selections for ReleaseKit-owned validation rather than repeating the
|
||||||
|
standard suite, selected vet/format/distribution checks, or Markdown navigation
|
||||||
|
checks in `AdditionalChecks` or profile `checks`. Keep release-note rules and
|
||||||
|
consumer-specific assertions—such as tests of maintained examples—in those
|
||||||
|
product-owned checks.
|
||||||
ReleaseKit calls `AdditionalChecks` once after shared validation when supplied.
|
ReleaseKit calls `AdditionalChecks` once after shared validation when supplied.
|
||||||
It does not download a different toolchain automatically. If race prerequisites
|
It does not download a different toolchain automatically. If race prerequisites
|
||||||
are unavailable, the `go-test` check fails rather than being skipped; a selected
|
are unavailable, the `go-test` check fails rather than being skipped; a selected
|
||||||
|
|||||||
@@ -78,6 +78,19 @@ blob size is its link text; gitlinks are omitted. This check measures only the
|
|||||||
captured candidate and reports one oversized file with its measurement as
|
captured candidate and reports one oversized file with its measurement as
|
||||||
`file-size`; it stops publication before product checks or tag creation.
|
`file-size`; it stops publication before product checks or tag creation.
|
||||||
|
|
||||||
|
Set `Validation.Markdown` to check committed local Markdown destinations and
|
||||||
|
supported heading fragments during publication. Nil disables the check; an
|
||||||
|
empty object enables it with outside-checkout links reported as out of scope.
|
||||||
|
Set `EnforceCheckoutBoundary` to fail those links. Submodule contents remain
|
||||||
|
out of scope under either setting. Observations use the request's output writer;
|
||||||
|
failures use `markdown-links` and stop publication before product checks or tag
|
||||||
|
creation.
|
||||||
|
|
||||||
|
ReleaseKit-owned selections replace duplicate generic suite, vet, formatting,
|
||||||
|
distribution, and Markdown checks in configured product checks. Keep
|
||||||
|
release-note rules and consumer-specific assertions, such as maintained-example
|
||||||
|
tests, in `AdditionalChecks` or profile `checks`.
|
||||||
|
|
||||||
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
If `Build` selects targets, ReleaseKit verifies them after the standard suite
|
||||||
and additional checks, before creating the tag. The operation then rechecks the
|
and additional checks, before creating the tag. The operation then rechecks the
|
||||||
candidate and remote guards, creates an annotated tag for the captured commit,
|
candidate and remote guards, creates an annotated tag for the captured commit,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ reports these implemented packages:
|
|||||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
| `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/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||||
| `internal/validation` | Shared Go validation with optional race, full-vet, and static package-compilation target selections, formatting, workspace-file and filesystem-replacement checks, per-blob size limits, internal Markdown parsing, destination and fragment checking, and per-document anchor caching; `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, formatting, workspace-file and filesystem-replacement checks, per-blob size limits, and selected Markdown destination and fragment checking; `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/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||||
@@ -78,8 +78,8 @@ not a separate release matrix:
|
|||||||
| Capability | Implementation owner | Existing evidence |
|
| Capability | Implementation owner | Existing evidence |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
| 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`, `TestRunCompilesTargetsAfterTestAndVetInSelectionOrder`, `TestRunAttributesTargetCompileFailureAndStops`, `TestRunCompilesSourceOnlyAndExecutablePackagesWithoutArtifacts`, `TestCandidateRequestMapsGoValidation`, `TestLoadSettingsReadsGoValidation`, `TestCheckUsesSelectedRaceSuiteBeforeAdditionalChecks`, `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce`, `TestDeliverPageOnlyCreatesAndReusesRelease` |
|
| 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` |
|
||||||
| Internal Markdown parsing, committed destinations, fragments, boundary observations, and causes | `internal/validation` | `TestParseMarkdownLinksAndSourceLocations`, `TestParseMarkdownDecodesEntitiesInDestinations`, `TestParseMarkdownHeadingAnchors`, `TestMarkdownCacheKeepsParsedDocumentsPerCandidatePath`, `TestCheckMarkdownLinksResolvesCommittedDestinationsAndFragments`, `TestCheckMarkdownLinksReportsMissingPathsAnchorsAndCycles`, `TestCheckMarkdownLinksUsesResolvedMarkdownIdentity`, `TestCheckMarkdownLinksReportsBoundaryAndSubmoduleObservations`, `TestCheckMarkdownLinksPreservesResolverCauseAndCancellation`, `TestCheckMarkdownLinksPreservesAnchorReadCause` |
|
| 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`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ The optional file-size check uses each blob's captured Git size, including
|
|||||||
symlink link-text bytes, and skips entries without a blob size such as gitlinks.
|
symlink link-text bytes, and skips entries without a blob size such as gitlinks.
|
||||||
It applies normalized literal file/directory exclusions with path-boundary
|
It applies normalized literal file/directory exclusions with path-boundary
|
||||||
matching, then returns the first oversized path with `FileSizeMeasurement`.
|
matching, then returns the first oversized path with `FileSizeMeasurement`.
|
||||||
|
The optional Markdown check follows file-size inspection and uses the same
|
||||||
|
candidate tree. It parses regular Markdown blobs, resolves local destinations,
|
||||||
|
and checks supported anchors; outside-checkout and submodule observations are
|
||||||
|
written to the shared output writer. A nil writer keeps library use quiet.
|
||||||
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
`CandidateTree` gets metadata and bytes from Git objects, reads blob bytes
|
||||||
lazily, and resolves destinations without consulting the worktree or
|
lazily, and resolves destinations without consulting the worktree or
|
||||||
neighboring files.
|
neighboring files.
|
||||||
@@ -61,8 +65,8 @@ paths, symlink cycles, and missing anchors produce `markdown-links` findings at
|
|||||||
the source use site. Outside-checkout links produce out-of-scope observations by
|
the source use site. Outside-checkout links produce out-of-scope observations by
|
||||||
default and findings when boundary enforcement is selected. Submodule-content
|
default and findings when boundary enforcement is selected. Submodule-content
|
||||||
observations remain out of scope under either setting. Observations go to the
|
observations remain out of scope under either setting. Observations go to the
|
||||||
provided writer; a nil writer keeps library use quiet. Public selection remains
|
provided writer; a nil writer keeps library use quiet. Public selection is
|
||||||
in the [implementation plan](../roadmap/implementation.md).
|
defined in the [configuration reference](../config.md) and GoDoc.
|
||||||
|
|
||||||
Committed-tree metadata and blob reads disable lazy fetching and Git transports
|
Committed-tree metadata and blob reads disable lazy fetching and Git transports
|
||||||
for those child commands. Missing local objects produce inspection errors;
|
for those child commands. Missing local objects produce inspection errors;
|
||||||
@@ -136,6 +140,7 @@ symlink expansion detects cycles, and traversal honors cancellation. Absolute
|
|||||||
destinations and symlink targets within the repository directory map back to
|
destinations and symlink targets within the repository directory map back to
|
||||||
candidate-relative paths; existence still comes from committed entries.
|
candidate-relative paths; existence still comes from committed entries.
|
||||||
|
|
||||||
The [implementation plan](../roadmap/implementation.md) records the remaining
|
The [implementation plan](../roadmap/implementation.md) records the completed
|
||||||
public Markdown selection and integration work. The internal checker uses the
|
Markdown integration. The public selection is owned by the facade and profile;
|
||||||
committed-file and destination-resolution primitives described here.
|
the internal checker uses the committed-file and destination-resolution
|
||||||
|
primitives described here.
|
||||||
|
|||||||
@@ -446,8 +446,8 @@ Undefined references and HTML prose remain governed by parser interpretation.
|
|||||||
|
|
||||||
**Completion:** Complete. The internal checker resolves committed destinations,
|
**Completion:** Complete. The internal checker resolves committed destinations,
|
||||||
checks supported fragments, preserves inspection errors, and reports distinct
|
checks supported fragments, preserves inspection errors, and reports distinct
|
||||||
outside-checkout and submodule observations. Public selection and shared executor
|
outside-checkout and submodule observations. Stage 11 exposes this checker
|
||||||
wiring remain in Stage 11.
|
through public and profile selections and the shared executor.
|
||||||
|
|
||||||
**Outcome:** Committed Markdown navigation is checked with boundary observations.
|
**Outcome:** Committed Markdown navigation is checked with boundary observations.
|
||||||
|
|
||||||
@@ -477,6 +477,14 @@ missing-object, and traversal-cancellation cases.
|
|||||||
|
|
||||||
## Stage 11: Expose Markdown selection and complete validation integration
|
## Stage 11: Expose Markdown selection and complete validation integration
|
||||||
|
|
||||||
|
**Completion:** Complete. `ValidationSpec.Markdown` and
|
||||||
|
`validation.markdown` select the shared committed-link checker through both
|
||||||
|
public operations and CLI profile commands. Omitted/null settings disable the
|
||||||
|
check; an empty object enables it, and the boundary option maps explicitly.
|
||||||
|
Warnings use the shared output writer, while Markdown failures stop product
|
||||||
|
checks and builds. GoDoc, profile, consumer, CLI, and internal documentation
|
||||||
|
now describe the operational behavior.
|
||||||
|
|
||||||
**Outcome:** All agreed validation selections work through both public operations.
|
**Outcome:** All agreed validation selections work through both public operations.
|
||||||
|
|
||||||
1. Add `MarkdownValidationSpec`, `ValidationSpec.Markdown`, and their JSON fields.
|
1. Add `MarkdownValidationSpec`, `ValidationSpec.Markdown`, and their JSON fields.
|
||||||
|
|||||||
@@ -45,12 +45,16 @@ func TestValidStableVersion(t *testing.T) {
|
|||||||
|
|
||||||
func TestCheckCapturesCandidateAndCallsAdditionalChecksOnce(t *testing.T) {
|
func TestCheckCapturesCandidateAndCallsAdditionalChecksOnce(t *testing.T) {
|
||||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||||
|
writeTestFile(t, repository, "guide.md", "# Guide\n[section](#guide)\n")
|
||||||
|
gitRun(t, repository, "add", "guide.md")
|
||||||
|
gitRun(t, repository, "commit", "-m", "add Markdown guide")
|
||||||
calls := 0
|
calls := 0
|
||||||
var seen Candidate
|
var seen Candidate
|
||||||
candidate, err := Check(context.Background(), Request{
|
candidate, err := Check(context.Background(), Request{
|
||||||
RepositoryDir: repository,
|
RepositoryDir: repository,
|
||||||
Product: "Example Product",
|
Product: "Example Product",
|
||||||
Version: testVersion,
|
Version: testVersion,
|
||||||
|
Validation: validation.Settings{Markdown: &validation.MarkdownSettings{}},
|
||||||
AdditionalChecks: func(_ context.Context, captured Candidate) error {
|
AdditionalChecks: func(_ context.Context, captured Candidate) error {
|
||||||
calls++
|
calls++
|
||||||
seen = captured
|
seen = captured
|
||||||
@@ -108,6 +112,38 @@ func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckStopsBeforeProductAndBuildCallbacksWhenMarkdownBoundaryFails(t *testing.T) {
|
||||||
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||||
|
writeTestFile(t, repository, "guide.md", "[outside](../../outside.md)\n")
|
||||||
|
gitRun(t, repository, "add", "guide.md")
|
||||||
|
gitRun(t, repository, "commit", "-m", "add outside Markdown destination")
|
||||||
|
productCalls := 0
|
||||||
|
buildCalls := 0
|
||||||
|
_, err := Check(context.Background(), Request{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Example Product",
|
||||||
|
Version: testVersion,
|
||||||
|
Validation: validation.Settings{
|
||||||
|
Markdown: &validation.MarkdownSettings{EnforceCheckoutBoundary: true},
|
||||||
|
},
|
||||||
|
AdditionalChecks: func(context.Context, Candidate) error {
|
||||||
|
productCalls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
AfterValidate: func(context.Context, Candidate) error {
|
||||||
|
buildCalls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
var failure *validation.Failure
|
||||||
|
if !errors.As(err, &failure) || failure.Check != validation.MarkdownLinks {
|
||||||
|
t.Fatalf("Check() error = %v, want markdown-links failure", err)
|
||||||
|
}
|
||||||
|
if productCalls != 0 || buildCalls != 0 {
|
||||||
|
t.Fatalf("product callback calls = %d, build callback calls = %d; want neither after Markdown failure", productCalls, buildCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckReportsCommittedWorkspaceFilesWhenGoWorkspaceIsDisabled(t *testing.T) {
|
func TestCheckReportsCommittedWorkspaceFilesWhenGoWorkspaceIsDisabled(t *testing.T) {
|
||||||
t.Setenv("GOWORK", "off")
|
t.Setenv("GOWORK", "off")
|
||||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||||
|
|||||||
@@ -192,6 +192,49 @@ func TestCheckMarkdownLinksPreservesAnchorReadCause(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunSelectsMarkdownWithCombinedGoChecksAndReportsOutOfScopeLinks(t *testing.T) {
|
||||||
|
repository, _ := committedMarkdownTree(t, map[string][]byte{
|
||||||
|
"guide.md": []byte("[outside](../../outside.md)\n"),
|
||||||
|
})
|
||||||
|
commit := strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD"))
|
||||||
|
goLog := installGoShim(t, "", false)
|
||||||
|
var output bytes.Buffer
|
||||||
|
err := Run(context.Background(), Request{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Commit: commit,
|
||||||
|
Output: &output,
|
||||||
|
Settings: Settings{
|
||||||
|
Race: true,
|
||||||
|
FullVet: true,
|
||||||
|
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}},
|
||||||
|
Markdown: &MarkdownSettings{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v, want out-of-scope observation to succeed", err)
|
||||||
|
}
|
||||||
|
if got := output.String(); !strings.Contains(got, "outside the checkout") || !strings.Contains(got, "out of scope") {
|
||||||
|
t.Fatalf("Run() output = %q, want outside-checkout observation", got)
|
||||||
|
}
|
||||||
|
commands := readGoShimLog(t, goLog)
|
||||||
|
var testCommands []string
|
||||||
|
var validationCommands []string
|
||||||
|
for _, command := range commands {
|
||||||
|
if strings.HasPrefix(command, "test ") {
|
||||||
|
testCommands = append(testCommands, command)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(command, "test ") || strings.HasPrefix(command, "vet ") || strings.HasPrefix(command, "build ") {
|
||||||
|
validationCommands = append(validationCommands, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(testCommands) != 1 || !strings.Contains(testCommands[0], "-race") || !strings.Contains(testCommands[0], "-vet=off") {
|
||||||
|
t.Fatalf("Go test commands = %#v, want one race-enabled suite with test-time vet disabled", testCommands)
|
||||||
|
}
|
||||||
|
if len(validationCommands) != 3 || !strings.HasPrefix(validationCommands[0], "test ") || !strings.HasPrefix(validationCommands[1], "vet ") || !strings.HasPrefix(validationCommands[2], "build ") {
|
||||||
|
t.Fatalf("Go validation commands = %#v, want test, full vet, and target compilation before Markdown", validationCommands)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func committedMarkdownTree(t *testing.T, files map[string][]byte) (string, *CandidateTree) {
|
func committedMarkdownTree(t *testing.T, files map[string][]byte) (string, *CandidateTree) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
repository := newTreeRepository(t)
|
repository := newTreeRepository(t)
|
||||||
|
|||||||
@@ -20,10 +20,16 @@ type Settings struct {
|
|||||||
FullVet bool
|
FullVet bool
|
||||||
CompileTargets []Target
|
CompileTargets []Target
|
||||||
GoFormat bool
|
GoFormat bool
|
||||||
|
Markdown *MarkdownSettings
|
||||||
ModuleDistribution ModuleDistributionSettings
|
ModuleDistribution ModuleDistributionSettings
|
||||||
FileSize *FileSizeSettings
|
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.
|
// FileSizeSettings selects a per-blob limit and literal path exclusions.
|
||||||
type FileSizeSettings struct {
|
type FileSizeSettings struct {
|
||||||
LimitBytes *int64
|
LimitBytes *int64
|
||||||
@@ -112,10 +118,13 @@ func Run(ctx context.Context, request Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if request.Settings.GoFormat || request.Settings.ModuleDistribution.WorkspaceFiles || request.Settings.ModuleDistribution.LocalReplacements || request.Settings.FileSize != nil {
|
if request.Settings.GoFormat || request.Settings.ModuleDistribution.WorkspaceFiles || request.Settings.ModuleDistribution.LocalReplacements || request.Settings.FileSize != nil || request.Settings.Markdown != nil {
|
||||||
tree, err := request.ReadCandidateTree(ctx)
|
tree, err := request.ReadCandidateTree(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
check := FileSize
|
check := MarkdownLinks
|
||||||
|
if request.Settings.FileSize != nil {
|
||||||
|
check = FileSize
|
||||||
|
}
|
||||||
if request.Settings.ModuleDistribution.LocalReplacements {
|
if request.Settings.ModuleDistribution.LocalReplacements {
|
||||||
check = LocalReplacements
|
check = LocalReplacements
|
||||||
}
|
}
|
||||||
@@ -147,6 +156,11 @@ func Run(ctx context.Context, request Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if request.Settings.Markdown != nil {
|
||||||
|
if err := checkMarkdownLinks(ctx, tree, request.Settings.Markdown.EnforceCheckoutBoundary, request.Output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
11
publish.go
11
publish.go
@@ -54,9 +54,14 @@ type PublicationResult struct {
|
|||||||
// Validation.Go.FullVet runs go vet afterward instead of test-time vet, and
|
// Validation.Go.FullVet runs go vet afterward instead of test-time vet, and
|
||||||
// Validation.Go.CompileTargets compiles all packages for each selected target
|
// Validation.Go.CompileTargets compiles all packages for each selected target
|
||||||
// with cgo disabled after tests and vet. Compilation produces no artifacts and
|
// with cgo disabled after tests and vet. Compilation produces no artifacts and
|
||||||
// does not execute target binaries. ExpectedPushURL, when nonempty, is compared
|
// does not execute target binaries. The selected formatting, distribution,
|
||||||
// exactly with the resolved effective push URL before validation; a mismatch
|
// file-size, and Markdown checks then run before AdditionalChecks. Markdown is
|
||||||
// returns ErrUnexpectedPushURL. It then calls AdditionalChecks when supplied.
|
// disabled when nil and enabled by a non-nil specification; outside-checkout
|
||||||
|
// destinations are reported as out of scope unless boundary enforcement is
|
||||||
|
// selected. Submodule contents remain out of scope. ExpectedPushURL, when
|
||||||
|
// nonempty, is compared exactly with the resolved effective push URL before
|
||||||
|
// validation; a mismatch returns ErrUnexpectedPushURL. It then calls
|
||||||
|
// AdditionalChecks when supplied.
|
||||||
// It optionally builds selected targets in temporary storage outside the
|
// It optionally builds selected targets in temporary storage outside the
|
||||||
// worktree and pushes the exact annotated tag object to the selected remote.
|
// worktree and pushes the exact annotated tag object to the selected remote.
|
||||||
// It returns a result with errors: preflight failures are not_attempted, failed
|
// It returns a result with errors: preflight failures are not_attempted, failed
|
||||||
|
|||||||
@@ -302,6 +302,62 @@ func TestCheckReturnsPublicValidationError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarkdownValidationRunsThroughCheckAndPublish(t *testing.T) {
|
||||||
|
addBrokenMarkdownDestination := func(t *testing.T, repo publishFixture) {
|
||||||
|
t.Helper()
|
||||||
|
writeFile(t, repo.work, "guide.md", "[missing](missing.md#top)\n")
|
||||||
|
gitRun(t, repo.work, "add", "guide.md")
|
||||||
|
gitRun(t, repo.work, "commit", "-m", "add broken Markdown destination")
|
||||||
|
}
|
||||||
|
assertMarkdownFailure := func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
var failure *releasekit.ValidationError
|
||||||
|
if !errors.As(err, &failure) || failure.Check != "markdown-links" {
|
||||||
|
t.Fatalf("operation error = %v, want public markdown-links failure", err)
|
||||||
|
}
|
||||||
|
if len(failure.Findings) != 1 || failure.Findings[0].Path != "guide.md" || failure.Findings[0].Target != "missing.md#top" || failure.Findings[0].Line != 1 {
|
||||||
|
t.Fatalf("Markdown findings = %#v, want source location and target", failure.Findings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("check", func(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
addBrokenMarkdownDestination(t, repo)
|
||||||
|
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Validation: releasekit.ValidationSpec{Markdown: &releasekit.MarkdownValidationSpec{}},
|
||||||
|
})
|
||||||
|
assertMarkdownFailure(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("publish", func(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
addBrokenMarkdownDestination(t, repo)
|
||||||
|
gitRun(t, repo.work, "config", "--unset", "remote.origin.mirror")
|
||||||
|
gitRun(t, repo.work, "push", "origin", "HEAD:refs/heads/main")
|
||||||
|
callbackCalls := 0
|
||||||
|
_, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Validation: releasekit.ValidationSpec{Markdown: &releasekit.MarkdownValidationSpec{}},
|
||||||
|
AdditionalChecks: func(context.Context, releasekit.Candidate) error {
|
||||||
|
callbackCalls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assertMarkdownFailure(t, err)
|
||||||
|
if callbackCalls != 0 {
|
||||||
|
t.Fatalf("additional checks called %d times after Markdown failure", callbackCalls)
|
||||||
|
}
|
||||||
|
if tag := gitText(t, repo.work, "tag", "--list", "v1.2.3"); tag != "" {
|
||||||
|
t.Fatalf("failed Markdown validation created tag %q", tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation(t *testing.T) {
|
func TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation(t *testing.T) {
|
||||||
repo := newPublishFixture(t)
|
repo := newPublishFixture(t)
|
||||||
suiteRuns := filepath.Join(t.TempDir(), "suite-runs")
|
suiteRuns := filepath.Join(t.TempDir(), "suite-runs")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// ValidationCheck identifies a shared validation check that failed.
|
// ValidationCheck identifies a shared validation check that failed.
|
||||||
// Current checks include "go-test", "go-vet", "package-compile", "go-format",
|
// Current checks include "go-test", "go-vet", "package-compile", "go-format",
|
||||||
// "workspace-files", "local-replacements", and "file-size".
|
// "workspace-files", "local-replacements", "file-size", and "markdown-links".
|
||||||
type ValidationCheck string
|
type ValidationCheck string
|
||||||
|
|
||||||
// ValidationFinding describes a failed check observation at an optional
|
// ValidationFinding describes a failed check observation at an optional
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
Validation: ValidationSpec{Go: GoValidationSpec{
|
Validation: ValidationSpec{Go: GoValidationSpec{
|
||||||
Race: true, FullVet: true,
|
Race: true, FullVet: true,
|
||||||
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
CompileTargets: []Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}},
|
||||||
}, GoFormat: true, ModuleDistribution: ModuleDistributionValidationSpec{WorkspaceFiles: true, LocalReplacements: true}, FileSize: &FileSizeValidationSpec{LimitBytes: &limit, Exclusions: []string{"assets"}}},
|
}, GoFormat: true, Markdown: &MarkdownValidationSpec{EnforceCheckoutBoundary: true}, ModuleDistribution: ModuleDistributionValidationSpec{WorkspaceFiles: true, LocalReplacements: true}, FileSize: &FileSizeValidationSpec{LimitBytes: &limit, Exclusions: []string{"assets"}}},
|
||||||
})
|
})
|
||||||
if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat || !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
if !request.Validation.Race || !request.Validation.FullVet || !request.Validation.GoFormat || !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
||||||
t.Fatalf("candidate request validation = %#v, want race, full vet, formatting, and distribution checks enabled", request.Validation)
|
t.Fatalf("candidate request validation = %#v, want race, full vet, formatting, and distribution checks enabled", request.Validation)
|
||||||
}
|
}
|
||||||
|
if request.Validation.Markdown == nil || !request.Validation.Markdown.EnforceCheckoutBoundary {
|
||||||
|
t.Fatalf("candidate request Markdown = %#v, want boundary enforcement enabled", request.Validation.Markdown)
|
||||||
|
}
|
||||||
if request.Validation.FileSize == nil || request.Validation.FileSize.LimitBytes == nil || *request.Validation.FileSize.LimitBytes != limit || len(request.Validation.FileSize.Exclusions) != 1 || request.Validation.FileSize.Exclusions[0] != "assets" {
|
if request.Validation.FileSize == nil || request.Validation.FileSize.LimitBytes == nil || *request.Validation.FileSize.LimitBytes != limit || len(request.Validation.FileSize.Exclusions) != 1 || request.Validation.FileSize.Exclusions[0] != "assets" {
|
||||||
t.Fatalf("candidate request file size = %#v, want configured limit and exclusions", request.Validation.FileSize)
|
t.Fatalf("candidate request file size = %#v, want configured limit and exclusions", request.Validation.FileSize)
|
||||||
}
|
}
|
||||||
@@ -32,6 +35,24 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCandidateRequestPreservesMarkdownObjectSelection(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
spec *MarkdownValidationSpec
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "omitted"},
|
||||||
|
{name: "empty object", spec: &MarkdownValidationSpec{}, want: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
request := candidateRequest(ReleaseRequest{Validation: ValidationSpec{Markdown: test.spec}})
|
||||||
|
if got := request.Validation.Markdown != nil; got != test.want {
|
||||||
|
t.Fatalf("Markdown selected = %t, want %t", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCandidateRequestPreservesFileSizeObjectSelection(t *testing.T) {
|
func TestCandidateRequestPreservesFileSizeObjectSelection(t *testing.T) {
|
||||||
disabled := candidateRequest(ReleaseRequest{})
|
disabled := candidateRequest(ReleaseRequest{})
|
||||||
enabled := candidateRequest(ReleaseRequest{Validation: ValidationSpec{FileSize: &FileSizeValidationSpec{}}})
|
enabled := candidateRequest(ReleaseRequest{Validation: ValidationSpec{FileSize: &FileSizeValidationSpec{}}})
|
||||||
|
|||||||
Reference in New Issue
Block a user