Carry module identity through candidate results
This commit is contained in:
22
build.go
22
build.go
@@ -22,11 +22,14 @@ type BuildSpec struct {
|
||||
|
||||
// BuildRequest contains the source and output settings for Build.
|
||||
type BuildRequest struct {
|
||||
RepositoryDir string // source repository whose current HEAD is recorded.
|
||||
Version string // stable vMAJOR.MINOR.PATCH value injected at link time.
|
||||
Spec BuildSpec // application package, artifact name, and targets.
|
||||
OutputDir string // output location; empty selects <repository>/dist.
|
||||
Output io.Writer // optional build progress destination.
|
||||
RepositoryDir string // source repository whose current HEAD is recorded.
|
||||
// ModuleDir selects a repository-relative module directory; empty selects
|
||||
// the repository root.
|
||||
ModuleDir string
|
||||
Version string // stable vMAJOR.MINOR.PATCH value injected at link time.
|
||||
Spec BuildSpec // application package, artifact name, and targets.
|
||||
OutputDir string // output location; empty selects <repository>/dist.
|
||||
Output io.Writer // optional build progress destination.
|
||||
}
|
||||
|
||||
// BuildArtifact identifies one generated binary and its SHA-256 digest.
|
||||
@@ -40,6 +43,9 @@ type BuildArtifact struct {
|
||||
// BuildResult describes the source and artifacts reached by Build.
|
||||
type BuildResult struct {
|
||||
RepositoryDir string // absolute source repository path.
|
||||
ModuleDir string // absolute selected module directory.
|
||||
ModulePath string // selected module's declared Go path.
|
||||
Tag string // complete tag derived for this version.
|
||||
CandidateCommit string // Git HEAD observed when the build started.
|
||||
// SourceVerified reports clean source and the same HEAD observed before and
|
||||
// after a completed build. It is required for binary delivery, not an attestation.
|
||||
@@ -62,6 +68,7 @@ type BuildResult struct {
|
||||
func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
||||
result, err := internalbuild.Build(ctx, internalbuild.Request{
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
ModuleDir: request.ModuleDir,
|
||||
Version: request.Version,
|
||||
Spec: *internalBuildSpec(&request.Spec),
|
||||
OutputDir: request.OutputDir,
|
||||
@@ -69,6 +76,9 @@ func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
||||
})
|
||||
public := BuildResult{
|
||||
RepositoryDir: result.RepositoryDir,
|
||||
ModuleDir: result.ModuleDir,
|
||||
ModulePath: result.ModulePath,
|
||||
Tag: result.Tag,
|
||||
CandidateCommit: result.CandidateCommit,
|
||||
SourceVerified: result.SourceVerified,
|
||||
Version: result.Version,
|
||||
@@ -105,7 +115,7 @@ func internalBuildSpec(spec *BuildSpec) *internalbuild.Spec {
|
||||
}
|
||||
|
||||
func internalBuildResult(result BuildResult) *internalbuild.Result {
|
||||
converted := &internalbuild.Result{RepositoryDir: result.RepositoryDir, CandidateCommit: result.CandidateCommit, SourceVerified: result.SourceVerified, Version: result.Version, MainPackage: result.MainPackage, BinaryName: result.BinaryName, OutputDir: result.OutputDir, Compiler: result.Compiler}
|
||||
converted := &internalbuild.Result{RepositoryDir: result.RepositoryDir, ModuleDir: result.ModuleDir, ModulePath: result.ModulePath, Tag: result.Tag, CandidateCommit: result.CandidateCommit, SourceVerified: result.SourceVerified, Version: result.Version, MainPackage: result.MainPackage, BinaryName: result.BinaryName, OutputDir: result.OutputDir, Compiler: result.Compiler}
|
||||
for _, target := range result.Targets {
|
||||
converted.Targets = append(converted.Targets, internalbuild.Target{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
|
||||
42
check.go
42
check.go
@@ -18,9 +18,13 @@ var ErrInvalidVersion = candidate.ErrInvalidVersion
|
||||
var ErrUnexpectedPushURL = publish.ErrUnexpectedPushURL
|
||||
|
||||
// Candidate identifies one committed local source revision selected for
|
||||
// validation.
|
||||
// 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
|
||||
}
|
||||
@@ -30,9 +34,12 @@ type Candidate struct {
|
||||
// no remote availability claim.
|
||||
type ReleaseRequest struct {
|
||||
RepositoryDir string
|
||||
Product string
|
||||
Version string
|
||||
Remote 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.
|
||||
@@ -107,9 +114,12 @@ type GoValidationSpec struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
@@ -139,19 +149,27 @@ type GoValidationSpec struct {
|
||||
func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
||||
checked, err := candidate.Check(ctx, candidateRequest(request))
|
||||
if err != nil {
|
||||
return Candidate{}, convertValidationError(err)
|
||||
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,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -203,10 +221,6 @@ func candidateChecks(checks func(context.Context, Candidate) error) func(context
|
||||
return nil
|
||||
}
|
||||
return func(ctx context.Context, captured candidate.Candidate) error {
|
||||
return checks(ctx, Candidate{
|
||||
RepositoryDir: captured.RepositoryDir,
|
||||
Commit: captured.Commit,
|
||||
Version: captured.Version,
|
||||
})
|
||||
return checks(ctx, publicCandidate(captured))
|
||||
}
|
||||
}
|
||||
|
||||
18
delivery.go
18
delivery.go
@@ -46,9 +46,12 @@ type DeliveryAsset struct {
|
||||
// Prebuilt result, also performs page-only delivery.
|
||||
type DeliveryRequest struct {
|
||||
RepositoryDir string
|
||||
Product string
|
||||
Version string
|
||||
Destination GiteaDestination
|
||||
// ModuleDir selects a repository-relative module directory; empty selects
|
||||
// the repository root.
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
Destination GiteaDestination
|
||||
// Build selects binaries to build in temporary storage outside the checkout.
|
||||
Build *BuildSpec
|
||||
// Prebuilt supplies an existing build whose files remain caller-owned.
|
||||
@@ -62,6 +65,10 @@ type DeliveryRequest struct {
|
||||
// current observations: an asset appears in at most one slice, and early
|
||||
// failures may leave some declared assets unreported.
|
||||
type DeliveryResult struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
ModulePath string
|
||||
Tag string
|
||||
Version string
|
||||
CandidateCommit string
|
||||
ReleaseURL string
|
||||
@@ -95,6 +102,7 @@ func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, erro
|
||||
}
|
||||
internalResult, err := internaldelivery.Deliver(ctx, internaldelivery.Request{
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
ModuleDir: request.ModuleDir,
|
||||
Product: request.Product,
|
||||
Version: request.Version,
|
||||
Destination: destination,
|
||||
@@ -102,6 +110,10 @@ func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, erro
|
||||
Prebuilt: prebuilt,
|
||||
Output: request.Output,
|
||||
})
|
||||
result.RepositoryDir = internalResult.RepositoryDir
|
||||
result.ModuleDir = internalResult.ModuleDir
|
||||
result.ModulePath = internalResult.ModulePath
|
||||
result.Tag = internalResult.Tag
|
||||
result.CandidateCommit = internalResult.CandidateCommit
|
||||
result.ReleaseURL = internalResult.ReleaseURL
|
||||
result.State = DeliveryState(internalResult.State)
|
||||
|
||||
@@ -215,6 +215,13 @@ later validation failures retain available module, version, tag, and commit
|
||||
evidence. An unrelated sibling worktree change still fails the repository guard.
|
||||
Run candidate/facade tests and `go run ./internal/apicheck`.
|
||||
|
||||
**Completed:** Added module selection and identity evidence to the facade and
|
||||
internal request/result types. Candidate capture now reads the selected
|
||||
committed declaration and module-local note, guards the complete local tag, and
|
||||
preserves available evidence through callbacks and later failures. Verified
|
||||
nested physical-major capture, sibling cleanliness, facade error evidence,
|
||||
`go test . ./internal/candidate`, and `go run ./internal/apicheck`.
|
||||
|
||||
## Stage 3: Publish and reconcile the complete selected tag
|
||||
|
||||
**Outcome:** Publication addresses one module's exact tag and retains its current
|
||||
|
||||
@@ -29,6 +29,7 @@ type Spec struct {
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
Version string
|
||||
Spec Spec
|
||||
OutputDir string
|
||||
@@ -44,6 +45,9 @@ type Artifact struct {
|
||||
|
||||
type Result struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
ModulePath string
|
||||
Tag string
|
||||
CandidateCommit string
|
||||
SourceVerified bool
|
||||
Version string
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
@@ -20,6 +22,9 @@ var ErrInvalidVersion = errors.New("invalid stable version")
|
||||
|
||||
type Candidate struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
ModulePath string
|
||||
Tag string
|
||||
Commit string
|
||||
Version string
|
||||
Note string
|
||||
@@ -27,6 +32,7 @@ type Candidate struct {
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
Validation validation.Settings
|
||||
@@ -38,18 +44,18 @@ type Request struct {
|
||||
func Check(ctx context.Context, request Request) (Candidate, error) {
|
||||
captured, err := Capture(ctx, request)
|
||||
if err != nil {
|
||||
return Candidate{}, err
|
||||
return captured, err
|
||||
}
|
||||
if err := Validate(ctx, request, captured); err != nil {
|
||||
return Candidate{}, err
|
||||
return captured, err
|
||||
}
|
||||
if request.AfterValidate != nil {
|
||||
if err := request.AfterValidate(ctx, captured); err != nil {
|
||||
return Candidate{}, fmt.Errorf("after candidate validation: %w", err)
|
||||
return captured, fmt.Errorf("after candidate validation: %w", err)
|
||||
}
|
||||
}
|
||||
if err := Recheck(ctx, captured, ""); err != nil {
|
||||
return Candidate{}, err
|
||||
return captured, err
|
||||
}
|
||||
return captured, nil
|
||||
}
|
||||
@@ -57,43 +63,58 @@ func Check(ctx context.Context, request Request) (Candidate, error) {
|
||||
// Capture validates request identity and records one clean local candidate,
|
||||
// including the note bytes from the captured commit. It does not run checks.
|
||||
func Capture(ctx context.Context, request Request) (Candidate, error) {
|
||||
captured := Candidate{Version: request.Version}
|
||||
if !validStableVersion(request.Version) {
|
||||
return Candidate{}, fmt.Errorf("%w: %q", ErrInvalidVersion, request.Version)
|
||||
return captured, fmt.Errorf("%w: %q", ErrInvalidVersion, request.Version)
|
||||
}
|
||||
if strings.TrimSpace(request.Product) == "" {
|
||||
return Candidate{}, errors.New("product is required")
|
||||
return captured, errors.New("product is required")
|
||||
}
|
||||
|
||||
repositoryDir, err := filepath.Abs(request.RepositoryDir)
|
||||
if err != nil {
|
||||
return Candidate{}, fmt.Errorf("resolve repository directory: %w", err)
|
||||
return captured, fmt.Errorf("resolve repository directory: %w", err)
|
||||
}
|
||||
captured.RepositoryDir = repositoryDir
|
||||
state, err := inspectLocalState(ctx, repositoryDir)
|
||||
if err != nil {
|
||||
return Candidate{}, err
|
||||
return captured, err
|
||||
}
|
||||
captured.Commit = state.commit
|
||||
if state.branch != "main" {
|
||||
return Candidate{}, fmt.Errorf("release candidate must be on main; current branch is %q", state.branch)
|
||||
return captured, fmt.Errorf("release candidate must be on main; current branch is %q", state.branch)
|
||||
}
|
||||
if state.status != "" {
|
||||
return Candidate{}, fmt.Errorf("release candidate worktree and index must be clean:\n%s", state.status)
|
||||
return captured, fmt.Errorf("release candidate worktree and index must be clean:\n%s", state.status)
|
||||
}
|
||||
|
||||
path := "docs/releases/" + request.Version + ".md"
|
||||
note, err := gitOutput(ctx, repositoryDir, "show", state.commit+":"+path)
|
||||
identitySelection, err := releaseidentity.ResolveSelection(repositoryDir, request.ModuleDir)
|
||||
if err != nil {
|
||||
return Candidate{}, fmt.Errorf("read committed release note %q: %w", path, err)
|
||||
return captured, fmt.Errorf("resolve selected module: %w", err)
|
||||
}
|
||||
captured.RepositoryDir = identitySelection.RepositoryDir
|
||||
captured.ModuleDir = identitySelection.ModuleDir
|
||||
identity, err := releaseidentity.FromCommittedSelection(ctx, identitySelection, request.Version, state.commit)
|
||||
if err != nil {
|
||||
return captured, fmt.Errorf("resolve selected module release identity: %w", err)
|
||||
}
|
||||
captured.ModulePath = identity.ModulePath
|
||||
captured.Tag = identity.Tag
|
||||
note, err := git.ReadCommitFile(ctx, identity.RepositoryDir, state.commit, identity.NotePath)
|
||||
if err != nil {
|
||||
return captured, fmt.Errorf("read committed release note %q: %w", identity.NotePath, err)
|
||||
}
|
||||
if err := validateNote(note, request.Product, request.Version); err != nil {
|
||||
return Candidate{}, fmt.Errorf("release note %q: %w", path, err)
|
||||
return captured, fmt.Errorf("release note %q: %w", identity.NotePath, err)
|
||||
}
|
||||
if exists, err := hasLocalTag(ctx, repositoryDir, request.Version); err != nil {
|
||||
return Candidate{}, fmt.Errorf("inspect local tag %q: %w", request.Version, err)
|
||||
if exists, err := hasLocalTag(ctx, repositoryDir, identity.Tag); err != nil {
|
||||
return captured, fmt.Errorf("inspect local tag %q: %w", identity.Tag, err)
|
||||
} else if exists {
|
||||
return Candidate{}, fmt.Errorf("local tag %q already exists", request.Version)
|
||||
return captured, fmt.Errorf("local tag %q already exists", identity.Tag)
|
||||
}
|
||||
|
||||
return Candidate{RepositoryDir: repositoryDir, Commit: state.commit, Version: request.Version, Note: string(note)}, nil
|
||||
captured.Note = string(note)
|
||||
return captured, nil
|
||||
}
|
||||
|
||||
// Validate runs the selected offline Go suite, followed by AdditionalChecks
|
||||
@@ -133,23 +154,23 @@ func Recheck(ctx context.Context, captured Candidate, expectedTagObject string)
|
||||
return fmt.Errorf("worktree or index changed:\n%s", current.status)
|
||||
}
|
||||
if expectedTagObject == "" {
|
||||
if exists, err := hasLocalTag(ctx, captured.RepositoryDir, captured.Version); err != nil {
|
||||
return fmt.Errorf("recheck local tag %q: %w", captured.Version, err)
|
||||
if exists, err := hasLocalTag(ctx, captured.RepositoryDir, captured.Tag); err != nil {
|
||||
return fmt.Errorf("recheck local tag %q: %w", captured.Tag, err)
|
||||
} else if exists {
|
||||
return fmt.Errorf("local tag %q appeared during validation", captured.Version)
|
||||
return fmt.Errorf("local tag %q appeared during validation", captured.Tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
object, err := gitOutput(ctx, captured.RepositoryDir, "rev-parse", "--verify", "refs/tags/"+captured.Version)
|
||||
object, err := gitOutput(ctx, captured.RepositoryDir, "rev-parse", "--verify", "refs/tags/"+captured.Tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read local tag %q: %w", captured.Version, err)
|
||||
return fmt.Errorf("read local tag %q: %w", captured.Tag, err)
|
||||
}
|
||||
tagType, err := gitOutput(ctx, captured.RepositoryDir, "cat-file", "-t", "refs/tags/"+captured.Version)
|
||||
tagType, err := gitOutput(ctx, captured.RepositoryDir, "cat-file", "-t", "refs/tags/"+captured.Tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect local tag %q: %w", captured.Version, err)
|
||||
return fmt.Errorf("inspect local tag %q: %w", captured.Tag, err)
|
||||
}
|
||||
if strings.TrimSpace(string(object)) != expectedTagObject || strings.TrimSpace(string(tagType)) != "tag" {
|
||||
return fmt.Errorf("local tag %q no longer identifies annotated tag object %s", captured.Version, expectedTagObject)
|
||||
return fmt.Errorf("local tag %q no longer identifies annotated tag object %s", captured.Tag, expectedTagObject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -199,10 +220,10 @@ func gitOutput(ctx context.Context, repositoryDir string, args ...string) ([]byt
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func hasLocalTag(ctx context.Context, repositoryDir, version string) (bool, error) {
|
||||
func hasLocalTag(ctx context.Context, repositoryDir, tag string) (bool, error) {
|
||||
var stderr bytes.Buffer
|
||||
err := process.Run(ctx, process.Invocation{
|
||||
Args: []string{"git", "show-ref", "--verify", "--quiet", "refs/tags/" + version},
|
||||
Args: []string{"git", "show-ref", "--verify", "--quiet", "refs/tags/" + tag},
|
||||
Dir: repositoryDir,
|
||||
Stderr: &stderr,
|
||||
})
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestCheckCapturesCandidateAndCallsAdditionalChecksOnce(t *testing.T) {
|
||||
if seen != candidate {
|
||||
t.Fatalf("callback candidate = %#v, returned candidate = %#v", seen, candidate)
|
||||
}
|
||||
if candidate.RepositoryDir != repository || candidate.Commit != commit || candidate.Version != testVersion {
|
||||
if candidate.RepositoryDir != repository || candidate.ModuleDir != repository || candidate.ModulePath != "example.com/candidate" || candidate.Tag != testVersion || candidate.Commit != commit || candidate.Version != testVersion {
|
||||
t.Fatalf("Check() candidate = %#v", candidate)
|
||||
}
|
||||
if tags := gitOutputForTest(t, repository, "tag", "--list"); tags != "" {
|
||||
@@ -82,6 +82,67 @@ func TestCheckCapturesCandidateAndCallsAdditionalChecksOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureUsesNestedMajorModuleNoteAndTag(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRoot release.\n")
|
||||
moduleDir := "integrations/ckconfig/v2"
|
||||
writeTestFile(t, repository, moduleDir+"/go.mod", "module example.com/ckconfig/v2\n\ngo 1.25.0\n")
|
||||
writeTestFile(t, repository, moduleDir+"/docs/releases/v2.3.4.md", "# Example Product v2.3.4\n\nNested release.\n")
|
||||
gitRun(t, repository, "add", moduleDir)
|
||||
gitRun(t, repository, "commit", "-m", "add nested major module")
|
||||
|
||||
captured, err := Capture(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
ModuleDir: moduleDir,
|
||||
Product: "Example Product",
|
||||
Version: "v2.3.4",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if captured.RepositoryDir != repository || captured.ModuleDir != filepath.Join(repository, moduleDir) || captured.ModulePath != "example.com/ckconfig/v2" || captured.Tag != "integrations/ckconfig/v2.3.4" {
|
||||
t.Fatalf("nested candidate identity = %#v", captured)
|
||||
}
|
||||
if !strings.Contains(captured.Note, "Nested release.") || strings.Contains(captured.Note, "Root release.") {
|
||||
t.Fatalf("captured note = %q, want nested module note", captured.Note)
|
||||
}
|
||||
|
||||
gitRun(t, repository, "tag", captured.Tag)
|
||||
conflicted, err := Capture(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
ModuleDir: moduleDir,
|
||||
Product: "Example Product",
|
||||
Version: "v2.3.4",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), captured.Tag) {
|
||||
t.Fatalf("Capture() with existing nested tag returned %#v, %v", conflicted, err)
|
||||
}
|
||||
if conflicted.ModulePath != captured.ModulePath || conflicted.Tag != captured.Tag || conflicted.Commit != captured.Commit {
|
||||
t.Fatalf("capture failure lost identity evidence: %#v", conflicted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureKeepsRepositoryWideCleanlinessGuardForNestedSelection(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n")
|
||||
moduleDir := "integrations/ckconfig"
|
||||
writeTestFile(t, repository, moduleDir+"/go.mod", "module example.com/ckconfig\n")
|
||||
writeTestFile(t, repository, moduleDir+"/docs/releases/"+testVersion+".md", "# Example Product v1.2.3\n")
|
||||
gitRun(t, repository, "add", moduleDir)
|
||||
gitRun(t, repository, "commit", "-m", "add nested module")
|
||||
writeTestFile(t, repository, "sibling/untracked.txt", "dirty\n")
|
||||
captured, err := Capture(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
ModuleDir: moduleDir,
|
||||
Product: "Example Product",
|
||||
Version: testVersion,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "must be clean") {
|
||||
t.Fatalf("Capture() = %#v, %v; want repository-wide dirty-state rejection", captured, err)
|
||||
}
|
||||
if captured.Commit == "" {
|
||||
t.Fatalf("dirty-state error lost captured commit: %#v", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStopsBeforeProductAndBuildCallbacksWhenGoFormatFails(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n\nRelease note.\n")
|
||||
writeTestFile(t, repository, "unformatted.go", "package candidate\nfunc Value( ) int{return 1}\n")
|
||||
@@ -423,7 +484,7 @@ func TestCheckRechecksHeadAndWorktreeAfterCallback(t *testing.T) {
|
||||
|
||||
func TestCheckRechecksLocalTagAfterCallback(t *testing.T) {
|
||||
repository := newRepository(t, "# Example Product v1.2.3\n")
|
||||
_, err := Check(context.Background(), Request{
|
||||
captured, err := Check(context.Background(), Request{
|
||||
RepositoryDir: repository,
|
||||
Product: "Example Product",
|
||||
Version: testVersion,
|
||||
@@ -435,6 +496,9 @@ func TestCheckRechecksLocalTagAfterCallback(t *testing.T) {
|
||||
if err == nil || !strings.Contains(err.Error(), "appeared during validation") {
|
||||
t.Fatalf("Check() error = %v, want tag recheck error", err)
|
||||
}
|
||||
if captured.RepositoryDir != repository || captured.ModuleDir != repository || captured.ModulePath != "example.com/candidate" || captured.Tag != testVersion || captured.Commit == "" {
|
||||
t.Fatalf("tag recheck failure lost candidate evidence: %#v", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func validRequest(repository string) Request {
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
Destination gitea.Destination
|
||||
@@ -38,6 +39,10 @@ type Request struct {
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
ModulePath string
|
||||
Tag string
|
||||
CandidateCommit string
|
||||
ReleaseURL string
|
||||
State State
|
||||
|
||||
@@ -42,6 +42,7 @@ var ErrUnexpectedPushURL = errors.New("unexpected publication push URL")
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
Product string
|
||||
Version string
|
||||
Remote string
|
||||
@@ -53,6 +54,10 @@ type Request struct {
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
ModulePath string
|
||||
Tag string
|
||||
Version string
|
||||
CandidateCommit string
|
||||
Endpoint string
|
||||
@@ -76,6 +81,7 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
}
|
||||
checks := candidate.Request{
|
||||
RepositoryDir: request.RepositoryDir,
|
||||
ModuleDir: request.ModuleDir,
|
||||
Product: request.Product,
|
||||
Version: request.Version,
|
||||
Validation: request.Validation,
|
||||
@@ -83,10 +89,14 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
Output: request.Output,
|
||||
}
|
||||
captured, err := candidate.Capture(ctx, checks)
|
||||
result.RepositoryDir = captured.RepositoryDir
|
||||
result.ModuleDir = captured.ModuleDir
|
||||
result.ModulePath = captured.ModulePath
|
||||
result.Tag = captured.Tag
|
||||
result.CandidateCommit = captured.Commit
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("capture release candidate: %w", err)
|
||||
}
|
||||
result.CandidateCommit = captured.Commit
|
||||
|
||||
endpoint, err := git.Resolve(ctx, captured.RepositoryDir, remoteName)
|
||||
if err != nil {
|
||||
|
||||
@@ -79,6 +79,12 @@ func FromCommitted(ctx context.Context, repositoryDir, moduleDir, version, commi
|
||||
if err != nil {
|
||||
return Identity{}, err
|
||||
}
|
||||
return FromCommittedSelection(ctx, selection, version, commit)
|
||||
}
|
||||
|
||||
// FromCommittedSelection derives identity from a previously resolved module
|
||||
// selection and the selected module declaration at a captured commit.
|
||||
func FromCommittedSelection(ctx context.Context, selection Selection, version, commit string) (Identity, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Identity{}, fmt.Errorf("resolve committed release identity: %w", err)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ const (
|
||||
|
||||
// PublicationResult describes the publication state reached by this call.
|
||||
type PublicationResult struct {
|
||||
RepositoryDir string // absolute source repository path.
|
||||
ModuleDir string // absolute selected module directory.
|
||||
ModulePath string // selected module's declared Go path.
|
||||
Tag string // complete selected Git tag.
|
||||
Version string // selected stable version
|
||||
CandidateCommit string // captured main commit
|
||||
Endpoint string // captured effective push URL
|
||||
@@ -75,6 +79,7 @@ func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, er
|
||||
candidateInput := candidateRequest(request)
|
||||
result, err := publish.Publish(ctx, publish.Request{
|
||||
RepositoryDir: candidateInput.RepositoryDir,
|
||||
ModuleDir: candidateInput.ModuleDir,
|
||||
Product: candidateInput.Product,
|
||||
Version: candidateInput.Version,
|
||||
Remote: request.Remote,
|
||||
@@ -85,6 +90,10 @@ func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, er
|
||||
Output: candidateInput.Output,
|
||||
})
|
||||
return PublicationResult{
|
||||
RepositoryDir: result.RepositoryDir,
|
||||
ModuleDir: result.ModuleDir,
|
||||
ModulePath: result.ModulePath,
|
||||
Tag: result.Tag,
|
||||
Version: result.Version,
|
||||
CandidateCommit: result.CandidateCommit,
|
||||
Endpoint: result.Endpoint,
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) {
|
||||
if readErr != nil || string(runs) != "ran\n" {
|
||||
return fmt.Errorf("standard suite runs before additional checks: contents=%q error=%v", runs, readErr)
|
||||
}
|
||||
if candidate.RepositoryDir != repo.work || candidate.Commit != commit || candidate.Version != "v1.2.3" {
|
||||
if candidate.RepositoryDir != repo.work || candidate.ModuleDir != repo.work || candidate.ModulePath != "example.com/releasekit-publish-fixture" || candidate.Tag != "v1.2.3" || candidate.Commit != commit || candidate.Version != "v1.2.3" {
|
||||
t.Fatalf("callback candidate = %#v", candidate)
|
||||
}
|
||||
return nil
|
||||
@@ -58,7 +58,7 @@ func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) {
|
||||
if result.State != releasekit.PublicationPublished || result.Phase != releasekit.PublicationComplete {
|
||||
t.Fatalf("publication result = %#v", result)
|
||||
}
|
||||
if result.CandidateCommit != commit || result.Endpoint != repo.bare || result.TagObject == "" {
|
||||
if result.RepositoryDir != repo.work || result.ModuleDir != repo.work || result.ModulePath != "example.com/releasekit-publish-fixture" || result.Tag != "v1.2.3" || result.CandidateCommit != commit || result.Endpoint != repo.bare || result.TagObject == "" {
|
||||
t.Fatalf("publication identity = %#v", result)
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func requireRacePrerequisites(t *testing.T) {
|
||||
func TestCheckReturnsPublicValidationError(t *testing.T) {
|
||||
repo := newPublishFixture(t)
|
||||
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
||||
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||
captured, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||
RepositoryDir: repo.work,
|
||||
Product: "Sample App",
|
||||
Version: "v1.2.3",
|
||||
@@ -300,6 +300,9 @@ func TestCheckReturnsPublicValidationError(t *testing.T) {
|
||||
if !errors.Is(err, exitErr) {
|
||||
t.Fatal("Check() error does not preserve the Go process error identity")
|
||||
}
|
||||
if captured.RepositoryDir != repo.work || captured.ModuleDir != repo.work || captured.ModulePath != "example.com/releasekit-publish-fixture" || captured.Tag != "v1.2.3" || captured.Commit != repo.commit || captured.Version != "v1.2.3" {
|
||||
t.Fatalf("Check() failure candidate = %#v, want captured identity alongside validation error", captured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownValidationRunsThroughCheckAndPublish(t *testing.T) {
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestCandidateOperationsPreserveCallbackValidationDiagnostics(t *testing.T)
|
||||
RepositoryDir: repo.work, Product: "Sample App", Version: "v1.2.3",
|
||||
AdditionalChecks: func(context.Context, releasekit.Candidate) error { return diagnostic },
|
||||
}
|
||||
_, checkError := releasekit.Check(t.Context(), request)
|
||||
candidate, checkError := releasekit.Check(t.Context(), request)
|
||||
result, publishError := releasekit.Publish(t.Context(), request)
|
||||
for _, err := range []error{checkError, publishError} {
|
||||
var got *releasekit.ValidationError
|
||||
@@ -56,7 +56,13 @@ func TestCandidateOperationsPreserveCallbackValidationDiagnostics(t *testing.T)
|
||||
}
|
||||
}
|
||||
}
|
||||
if candidate.ModuleDir != repo.work || candidate.ModulePath != "example.com/releasekit-publish-fixture" || candidate.Tag != "v1.2.3" || candidate.Commit != repo.commit {
|
||||
t.Fatalf("Check() callback failure candidate = %#v, want captured identity", candidate)
|
||||
}
|
||||
if result.State != releasekit.PublicationNotAttempted || result.TagObject != "" {
|
||||
t.Fatalf("callback failure publication result = %#v", result)
|
||||
}
|
||||
if result.RepositoryDir != repo.work || result.ModuleDir != repo.work || result.ModulePath != candidate.ModulePath || result.Tag != candidate.Tag || result.CandidateCommit != candidate.Commit {
|
||||
t.Fatalf("Publish() callback failure result = %#v, want captured identity", result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user